Skip to content

Commit

Permalink
Merge pull request #32 from faradayio/hk/nullable-option
Browse files Browse the repository at this point in the history
Support 3.0.X nullability
  • Loading branch information
emk authored Jun 16, 2022
2 parents b80eabc + 3699c41 commit d2c2bbc
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 118 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed

- `--avoid-type-null` is now deprecated, and acts as an alias for `--use_nullable_for_merge_patch`. This flag now generates complete OpenAPI 3.0-compatible MergePatch types using a clever hack based on `nullable: true` and `oneOf:`. It also now sets the file version number to `openapi: 3.0.0` when used, because 3.1 does not support `nullable: true`.

## 0.3.0 - 2022-06-13

### Added
Expand Down
52 changes: 52 additions & 0 deletions examples/example_nullable_fields.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Testing the --use-deprecated-nullable option

openapi: "3.1.0"
info:
title: Example OpenAPI definition
components:
schemas:
Polygon:
type: object
properties:
name:
mutable: true
type: string
number_of_sides:
mutable: true
type: number

interfaces:
Resource:
emit: false # Do not include this in generated output.
members:
id:
required: true
schema:
type: string
Widget:
$includes: "Resource"
description: |
A displayable widget.
members:
# We can override properties from `Resource` using JSON
# Merge Patch syntax.
id:
schema:
example: e35a3c8d-5486-49ec-9b23-6747afc19570
name:
required: true
# We want to include a test for primitive types
mutable: true
schema:
type: string
comment:
mutable: true
schema:
type: string
shape:
# We also want to include a test for properties that are
# $refs. If mutable, this will be turned into a oneOf object
# where nullable: true.
mutable: true
schema:
$ref: "#/components/schemas/Polygon"
74 changes: 74 additions & 0 deletions examples/example_nullable_fields_output.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# AUTOMATICALLY GENERATED. DO NOT EDIT.
---
openapi: 3.0.0
paths: {}
components:
schemas:
Polygon:
type: object
properties:
name:
type: string
mutable: true
number_of_sides:
type: number
mutable: true
Widget:
type: object
required:
- id
- name
properties:
comment:
type: string
id:
type: string
example: e35a3c8d-5486-49ec-9b23-6747afc19570
name:
type: string
shape:
$ref: "#/components/schemas/Polygon"
additionalProperties: false
description: "A displayable widget.\n"
WidgetMergePatch:
type: object
properties:
comment:
type: string
nullable: true
name:
type: string
shape:
oneOf:
- $ref: "#/components/schemas/Polygon"
nullable: true
additionalProperties: false
description: "(Parameters used to PATCH the `Widget` type.)\n\nA displayable widget.\n"
WidgetPost:
type: object
required:
- name
properties:
comment:
type: string
name:
type: string
shape:
$ref: "#/components/schemas/Polygon"
additionalProperties: false
description: "(Parameters used to POST a new value of the `Widget` type.)\n\nA displayable widget.\n"
WidgetPut:
type: object
required:
- name
properties:
comment:
type: string
name:
type: string
shape:
$ref: "#/components/schemas/Polygon"
additionalProperties: false
description: "(Parameters used to PUT a value of the `Widget` type.)\n\nA displayable widget.\n"
info:
title: Example OpenAPI definition
15 changes: 6 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,10 @@ struct Opt {
#[structopt(short = "o", long = "out-file")]
output: Option<PathBuf>,

/// Do not introduce `type: "null"` in the output. This is automatic for
/// OpenAPI 3.0. This option will result in generic `MergePatch` types.
///
/// Useful for compatibility with readme.com and other OpenAPI 3.0-only
/// tools.
#[structopt(long = "avoid-type-null")]
avoid_type_null: bool,
/// Do not introduce `type: "null"` in the output. Instead, use `nullable:
/// true` and set `openapi: "3.0.0"` in the output.
#[structopt(long = "use-nullable-for-merge-patch", alias = "avoid-type-null")]
use_nullable_for_merge_patch: bool,
}

/// Main entry point. Really just a wrapper for `run` which sets up logging and
Expand All @@ -59,9 +56,9 @@ fn main() {
fn run(opt: &Opt) -> Result<()> {
let mut openapi = OpenApi::from_path(&opt.input)?;
let mut scope = Scope::default();
if !openapi.supports_type_null() || opt.avoid_type_null {
if !openapi.supports_type_null() || opt.use_nullable_for_merge_patch {
// We don't support `type: "null"`, so don't introduce it.
scope.use_generic_merge_patch_types = true;
scope.use_nullable_for_merge_patch = true;
}
trace!("Parsed: {:#?}", openapi);
resolve_included_files(&mut openapi, &opt.input)?;
Expand Down
74 changes: 6 additions & 68 deletions src/openapi/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,29 +454,6 @@ impl TranspileInterface for BasicInterface {
let mut types = BTreeSet::new();
types.insert(Type::Object);

// Check to see whether we should just generate a placeholder.
if scope.use_generic_merge_patch_types
&& variant == InterfaceVariant::MergePatch
{
let schema = PrimitiveSchema {
types,
required: vec![],
properties: Default::default(),
additional_properties: AdditionalProperties::Bool(true),
items: None,
nullable: None,
description: Some(format!(
"A patch to `{}Put` in JSON Merge Patch format (RFC 7396).",
name
)),
title: None,
r#const: None,
example: None,
unknown_fields: BTreeMap::default(),
};
return Ok(RefOr::Value(BasicSchema::Primitive(Box::new(schema))));
}

// Build our properties.
let mut required = vec![];
let mut properties = BTreeMap::new();
Expand Down Expand Up @@ -562,50 +539,6 @@ impl TranspileInterface for BasicInterface {
}
}

#[test]
fn generates_generic_merge_patch_types_when_necessary() {
let mut members = BTreeMap::new();
members.insert(
"field".to_owned(),
Member {
required: false,
mutable: true,
initializable: None,
// Literally any schema would work here.
schema: Schema::new_schema_matching_only_null_for_merge_patch(),
},
);
let iface = BasicInterface {
emit: true,
members,
additional_members: None,
discriminator_member_name: None,
description: None,
title: None,
example: None,
};

let scope = Scope {
use_generic_merge_patch_types: true,
..Scope::default()
};
let generated = iface
.generate_schema_variant(
&scope,
&BTreeMap::default(),
"Example",
InterfaceVariant::MergePatch,
)
.unwrap();

let expected_yaml = r#"
type: object
description: A patch to `ExamplePut` in JSON Merge Patch format (RFC 7396).
"#;
let expected = serde_yaml::from_str(expected_yaml).unwrap();
assert_eq!(generated, expected);
}

/// A member of an interface. Analogous to a property, but with more metadata
/// and a few restrictions.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
Expand Down Expand Up @@ -680,7 +613,11 @@ impl Member {
Some(schema)
} else {
// Optional fields become nullable.
Some(schema.new_schema_matching_current_or_null_for_merge_patch())
Some(
schema.new_schema_matching_current_or_null_for_merge_patch(
&scope,
),
)
}
}
InterfaceVariant::MergePatch => None,
Expand Down Expand Up @@ -802,6 +739,7 @@ impl TranspileInterface for OneOfInterface {
description: self.description.clone(),
title: self.title.clone(),
discriminator: Some(discriminator),
nullable: None,
unknown_fields: Default::default(),
})))
}
Expand Down
28 changes: 27 additions & 1 deletion src/openapi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,16 @@ impl Transpile for OpenApi {
type Output = Self;

fn transpile(&self, scope: &Scope) -> Result<Self> {
let openapi =
if scope.use_nullable_for_merge_patch && self.supports_type_null() {
// Always force version 3.0 if we were asked to use `nullable`
// but our existing version supports `type: null` instead.
Version::new(3, 0, 0)
} else {
self.openapi.clone()
};
Ok(Self {
openapi: self.openapi.clone(),
openapi,
include_files: Default::default(),
paths: self.paths.transpile(scope)?,
components: self.components.transpile(scope)?,
Expand Down Expand Up @@ -359,3 +367,21 @@ fn parses_include_file_example() {
OpenApi::from_path(Path::new("./examples/include_files/output.yml")).unwrap();
assert_eq!(transpiled, expected);
}

#[test]
fn parses_nullable_example() {
use pretty_assertions::assert_eq;

let path = Path::new("./examples/example_nullable_fields.yml").to_owned();
let parsed = OpenApi::from_path(&path).unwrap();
//println!("{:#?}", parsed);
let scope = Scope {
use_nullable_for_merge_patch: true,
..Scope::default()
};
let transpiled = parsed.transpile(&scope).unwrap();
let expected =
OpenApi::from_path(Path::new("./examples/example_nullable_fields_output.yml"))
.unwrap();
assert_eq!(transpiled, expected);
}
Loading

0 comments on commit d2c2bbc

Please sign in to comment.