Change $ref to be part of a SchemaObject.
This allows other keyworlds to be used alongside $ref, as allowed in Json Schema 2019-09
This commit is contained in:
parent
7d162a8fb5
commit
5a82498e28
6 changed files with 115 additions and 137 deletions
|
@ -4,9 +4,9 @@ use crate::{JsonSchemaError, Map, Result, Set};
|
||||||
impl Schema {
|
impl Schema {
|
||||||
pub fn flatten(self, other: Self) -> Result {
|
pub fn flatten(self, other: Self) -> Result {
|
||||||
if is_null_type(&self) {
|
if is_null_type(&self) {
|
||||||
return Ok(other)
|
return Ok(other);
|
||||||
} else if is_null_type(&other) {
|
} else if is_null_type(&other) {
|
||||||
return Ok(self)
|
return Ok(self);
|
||||||
}
|
}
|
||||||
let s1 = ensure_object_type(self)?;
|
let s1 = ensure_object_type(self)?;
|
||||||
let s2 = ensure_object_type(other)?;
|
let s2 = ensure_object_type(other)?;
|
||||||
|
@ -38,7 +38,7 @@ impl_merge!(SchemaObject {
|
||||||
merge: definitions extensions instance_type enum_values
|
merge: definitions extensions instance_type enum_values
|
||||||
number string array object,
|
number string array object,
|
||||||
or: schema id title description format const_value all_of any_of one_of not
|
or: schema id title description format const_value all_of any_of one_of not
|
||||||
if_schema then_schema else_schema,
|
if_schema then_schema else_schema reference,
|
||||||
});
|
});
|
||||||
|
|
||||||
impl_merge!(NumberValidation {
|
impl_merge!(NumberValidation {
|
||||||
|
|
|
@ -18,6 +18,16 @@ pub enum BoolSchemas {
|
||||||
|
|
||||||
impl Default for SchemaSettings {
|
impl Default for SchemaSettings {
|
||||||
fn default() -> SchemaSettings {
|
fn default() -> SchemaSettings {
|
||||||
|
SchemaSettings::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SchemaSettings {
|
||||||
|
pub fn new() -> SchemaSettings {
|
||||||
|
Self::draft07()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draft07() -> SchemaSettings {
|
||||||
SchemaSettings {
|
SchemaSettings {
|
||||||
option_nullable: false,
|
option_nullable: false,
|
||||||
option_add_null_type: true,
|
option_add_null_type: true,
|
||||||
|
@ -25,14 +35,7 @@ impl Default for SchemaSettings {
|
||||||
definitions_path: "#/definitions/".to_owned(),
|
definitions_path: "#/definitions/".to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl SchemaSettings {
|
|
||||||
pub fn new() -> SchemaSettings {
|
|
||||||
SchemaSettings {
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn openapi3() -> SchemaSettings {
|
pub fn openapi3() -> SchemaSettings {
|
||||||
SchemaSettings {
|
SchemaSettings {
|
||||||
option_nullable: true,
|
option_nullable: true,
|
||||||
|
@ -66,21 +69,20 @@ impl SchemaGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn schema_for_any(&self) -> Schema {
|
pub fn schema_for_any(&self) -> Schema {
|
||||||
|
let schema: Schema = true.into();
|
||||||
if self.settings().bool_schemas == BoolSchemas::Enable {
|
if self.settings().bool_schemas == BoolSchemas::Enable {
|
||||||
true.into()
|
schema
|
||||||
} else {
|
} else {
|
||||||
Schema::Object(Default::default())
|
Schema::Object(schema.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn schema_for_none(&self) -> Schema {
|
pub fn schema_for_none(&self) -> Schema {
|
||||||
|
let schema: Schema = false.into();
|
||||||
if self.settings().bool_schemas == BoolSchemas::Enable {
|
if self.settings().bool_schemas == BoolSchemas::Enable {
|
||||||
false.into()
|
schema
|
||||||
} else {
|
} else {
|
||||||
Schema::Object(SchemaObject {
|
Schema::Object(schema.into())
|
||||||
not: Some(Schema::Object(Default::default()).into()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +96,7 @@ impl SchemaGenerator {
|
||||||
if !self.definitions.contains_key(&name) {
|
if !self.definitions.contains_key(&name) {
|
||||||
self.insert_new_subschema_for::<T>(name)?;
|
self.insert_new_subschema_for::<T>(name)?;
|
||||||
}
|
}
|
||||||
Ok(Ref { reference }.into())
|
Ok(Schema::new_ref(reference))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_new_subschema_for<T: ?Sized + JsonSchema>(&mut self, name: String) -> Result<()> {
|
fn insert_new_subschema_for<T: ?Sized + JsonSchema>(&mut self, name: String) -> Result<()> {
|
||||||
|
@ -148,49 +150,41 @@ impl SchemaGenerator {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_schema_object(&self, mut schema: Schema) -> Result<SchemaObject> {
|
pub fn dereference_once(&self, schema: Schema) -> Result<Schema> {
|
||||||
for _ in 0..100 {
|
|
||||||
match schema {
|
match schema {
|
||||||
Schema::Object(obj) => return Ok(obj),
|
Schema::Object(SchemaObject {
|
||||||
Schema::Bool(true) => return Ok(Default::default()),
|
reference: Some(ref schema_ref),
|
||||||
Schema::Bool(false) => {
|
..
|
||||||
return Ok(SchemaObject {
|
}) => {
|
||||||
not: Some(Schema::Bool(true).into()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Schema::Ref(schema_ref) => {
|
|
||||||
let definitions_path = &self.settings().definitions_path;
|
let definitions_path = &self.settings().definitions_path;
|
||||||
if !schema_ref.reference.starts_with(definitions_path) {
|
if !schema_ref.starts_with(definitions_path) {
|
||||||
return Err(JsonSchemaError::new(
|
return Err(JsonSchemaError::new(
|
||||||
"Could not extract referenced schema name.",
|
"Could not extract referenced schema name.",
|
||||||
Schema::Ref(schema_ref),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = &schema_ref.reference[definitions_path.len()..];
|
|
||||||
schema = self
|
|
||||||
.definitions
|
|
||||||
.get(name)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
JsonSchemaError::new(
|
|
||||||
"Could not find referenced schema.",
|
|
||||||
Schema::Ref(schema_ref.clone()),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
if schema == Schema::Ref(schema_ref) {
|
|
||||||
return Err(JsonSchemaError::new(
|
|
||||||
"Schema is referencing itself.",
|
|
||||||
schema,
|
schema,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let name = &schema_ref[definitions_path.len()..];
|
||||||
|
self.definitions.get(name).cloned().ok_or_else(|| {
|
||||||
|
JsonSchemaError::new("Could not find referenced schema.", schema)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
s => Ok(s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dereference(&self, mut schema: Schema) -> Result<Schema> {
|
||||||
|
if !schema.is_ref() {
|
||||||
|
return Ok(schema);
|
||||||
|
}
|
||||||
|
for _ in 0..100 {
|
||||||
|
schema = self.dereference_once(schema)?;
|
||||||
|
if !schema.is_ref() {
|
||||||
|
return Ok(schema);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(JsonSchemaError::new(
|
Err(JsonSchemaError::new(
|
||||||
"Failed to dereference schema after 100 iterations - reference may be cyclic.",
|
"Failed to dereference schema after 100 iterations - references may be cyclic.",
|
||||||
schema,
|
schema,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ impl<T: JsonSchema> JsonSchema for Option<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if gen.settings().option_nullable {
|
if gen.settings().option_nullable {
|
||||||
let mut deref = gen.get_schema_object(schema)?;
|
let mut deref: SchemaObject = gen.dereference(schema)?.into();
|
||||||
deref.extensions.insert("nullable".to_owned(), json!(true));
|
deref.extensions.insert("nullable".to_owned(), json!(true));
|
||||||
schema = Schema::Object(deref);
|
schema = Schema::Object(deref);
|
||||||
};
|
};
|
||||||
|
@ -110,12 +110,7 @@ mod tests {
|
||||||
assert_eq!(schema.any_of.is_some(), true);
|
assert_eq!(schema.any_of.is_some(), true);
|
||||||
let any_of = schema.any_of.unwrap();
|
let any_of = schema.any_of.unwrap();
|
||||||
assert_eq!(any_of.len(), 2);
|
assert_eq!(any_of.len(), 2);
|
||||||
assert_eq!(
|
assert_eq!(any_of[0], Schema::new_ref("#/definitions/Foo".to_string()));
|
||||||
any_of[0],
|
|
||||||
Schema::Ref(Ref {
|
|
||||||
reference: "#/definitions/Foo".to_string()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(any_of[1], schema_for::<()>());
|
assert_eq!(any_of[1], schema_for::<()>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,22 @@ use serde_json::Value;
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum Schema {
|
pub enum Schema {
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
Ref(Ref),
|
|
||||||
Object(SchemaObject),
|
Object(SchemaObject),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Schema {
|
||||||
|
pub fn new_ref(reference: String) -> Self {
|
||||||
|
SchemaObject::new_ref(reference).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_ref(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Schema::Object(o) => o.is_ref(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<SchemaObject> for Schema {
|
impl From<SchemaObject> for Schema {
|
||||||
fn from(o: SchemaObject) -> Self {
|
fn from(o: SchemaObject) -> Self {
|
||||||
Schema::Object(o)
|
Schema::Object(o)
|
||||||
|
@ -23,18 +35,6 @@ impl From<bool> for Schema {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Ref> for Schema {
|
|
||||||
fn from(r: Ref) -> Self {
|
|
||||||
Schema::Ref(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
|
|
||||||
pub struct Ref {
|
|
||||||
#[serde(rename = "$ref")]
|
|
||||||
pub reference: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", default)]
|
#[serde(rename_all = "camelCase", default)]
|
||||||
pub struct SchemaObject {
|
pub struct SchemaObject {
|
||||||
|
@ -68,7 +68,7 @@ pub struct SchemaObject {
|
||||||
pub then_schema: Option<Box<Schema>>,
|
pub then_schema: Option<Box<Schema>>,
|
||||||
#[serde(rename = "else", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "else", skip_serializing_if = "Option::is_none")]
|
||||||
pub else_schema: Option<Box<Schema>>,
|
pub else_schema: Option<Box<Schema>>,
|
||||||
#[serde(skip_serializing_if = "Map::is_empty")]
|
#[serde(alias = "$defs", skip_serializing_if = "Map::is_empty")]
|
||||||
pub definitions: Map<String, Schema>,
|
pub definitions: Map<String, Schema>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub number: NumberValidation,
|
pub number: NumberValidation,
|
||||||
|
@ -78,10 +78,45 @@ pub struct SchemaObject {
|
||||||
pub array: ArrayValidation,
|
pub array: ArrayValidation,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub object: ObjectValidation,
|
pub object: ObjectValidation,
|
||||||
|
#[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reference: Option<String>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub extensions: Map<String, Value>,
|
pub extensions: Map<String, Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SchemaObject {
|
||||||
|
pub fn new_ref(reference: String) -> Self {
|
||||||
|
SchemaObject {
|
||||||
|
reference: Some(reference),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_ref(&self) -> bool {
|
||||||
|
if self.reference.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let only_ref = SchemaObject {
|
||||||
|
reference: self.reference.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
*self == only_ref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Schema> for SchemaObject {
|
||||||
|
fn from(schema: Schema) -> Self {
|
||||||
|
match schema {
|
||||||
|
Schema::Object(o) => o,
|
||||||
|
Schema::Bool(true) => SchemaObject::default(),
|
||||||
|
Schema::Bool(false) => SchemaObject {
|
||||||
|
not: Some(Schema::Object(Default::default()).into()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", default)]
|
#[serde(rename_all = "camelCase", default)]
|
||||||
pub struct NumberValidation {
|
pub struct NumberValidation {
|
||||||
|
@ -144,7 +179,9 @@ pub struct ObjectValidation {
|
||||||
pub property_names: Option<Box<Schema>>,
|
pub property_names: Option<Box<Schema>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
|
#[derive(
|
||||||
|
Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema,
|
||||||
|
)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum InstanceType {
|
pub enum InstanceType {
|
||||||
Null,
|
Null,
|
||||||
|
|
|
@ -5,9 +5,6 @@
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/SchemaObject"
|
"$ref": "#/components/schemas/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -24,25 +21,11 @@
|
||||||
"integer"
|
"integer"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Ref": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"$ref"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"$ref": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Schema": {
|
"Schema": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/SchemaObject"
|
"$ref": "#/components/schemas/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -55,6 +38,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"$ref": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"$schema": {
|
"$schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
@ -64,9 +51,6 @@
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/SchemaObject"
|
"$ref": "#/components/schemas/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -78,9 +62,6 @@
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/SchemaObject"
|
"$ref": "#/components/schemas/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -109,9 +90,6 @@
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/SchemaObject"
|
"$ref": "#/components/schemas/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -133,9 +111,6 @@
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/SchemaObject"
|
"$ref": "#/components/schemas/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -166,9 +141,6 @@
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/SchemaObject"
|
"$ref": "#/components/schemas/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -239,9 +211,6 @@
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/SchemaObject"
|
"$ref": "#/components/schemas/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -276,9 +245,6 @@
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/SchemaObject"
|
"$ref": "#/components/schemas/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -296,9 +262,6 @@
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/SchemaObject"
|
"$ref": "#/components/schemas/SchemaObject"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,6 @@
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/definitions/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/SchemaObject"
|
"$ref": "#/definitions/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -24,25 +21,11 @@
|
||||||
"integer"
|
"integer"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Ref": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"$ref"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"$ref": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Schema": {
|
"Schema": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/definitions/Ref"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/SchemaObject"
|
"$ref": "#/definitions/SchemaObject"
|
||||||
}
|
}
|
||||||
|
@ -57,6 +40,12 @@
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"$ref": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"$schema": {
|
"$schema": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue