Skip to content

Commit

Permalink
fix(TYpings): Allow override of property optionality
Browse files Browse the repository at this point in the history
  • Loading branch information
nokome committed May 21, 2021
1 parent 32c607e commit ac24ef7
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 48 deletions.
8 changes: 4 additions & 4 deletions node/src/types.ts
Expand Up @@ -72,7 +72,7 @@ export interface Project {
/**
* The files in the project folder
*/
files?: Record<string, File>
files: Record<string, File>
/**
* The path (within the project) of the project's image
*
Expand Down Expand Up @@ -100,7 +100,7 @@ export interface Project {
/**
* The filesystem path of the project folder
*/
path?: string
path: string
/**
* The default theme to use when viewing documents in this project
*
Expand Down Expand Up @@ -174,7 +174,7 @@ export interface FileEvent {
*
* Will be `None` for for `refreshed` and `removed` events, or if for some reason it was not possible to fetch metadata about the file.
*/
file: File | undefined
file?: File
/**
* The updated set of files in the project
*
Expand Down Expand Up @@ -238,7 +238,7 @@ export interface Plugin {
*
* If the plugin is installed and there is a newer version of the plugin then this property should be set at the time of refresh.
*/
next: Plugin | undefined
next?: Plugin
/**
* The last time that the plugin manifest was updated. Used to determine if a refresh is necessary.
*/
Expand Down
4 changes: 2 additions & 2 deletions rust/src/files.rs
Expand Up @@ -178,12 +178,12 @@ pub struct FileEvent {
impl FileEvent {
/// Generate the JSON Schema for the `file` property
fn schema_file(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
schemas::typescript("File | undefined")
schemas::typescript("File", false)
}

/// Generate the JSON Schema for the `files` property
fn schema_files(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
schemas::typescript("Record<string, File>")
schemas::typescript("Record<string, File>", true)
}

pub fn publish(
Expand Down
2 changes: 1 addition & 1 deletion rust/src/plugins.rs
Expand Up @@ -119,7 +119,7 @@ impl Plugin {
/// self-referencing / recursive type. So here, we specify the
/// TypeScript type to use.
fn schema_next(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
schemas::typescript("Plugin | undefined")
schemas::typescript("Plugin", false)
}

/// Create a Markdown document describing a plugin
Expand Down
12 changes: 10 additions & 2 deletions rust/src/projects.rs
Expand Up @@ -57,6 +57,7 @@ pub struct Project {
// and should never be read from, or written to, the `project.json` file
/// The filesystem path of the project folder
#[serde(skip_deserializing)]
#[schemars(schema_with = "Project::schema_path")]
path: PathBuf,

/// The resolved path of the project's image file
Expand All @@ -74,9 +75,16 @@ pub struct Project {
}

impl Project {
/// Generate the JSON Schema for the `file` property
/// Generate the JSON Schema for the `path` property to avoid optionality
/// due to `skip_deserializing`
fn schema_path(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
schemas::typescript("string", true)
}

/// Generate the JSON Schema for the `file` property to avoid duplicated
/// inline type and optionality due to `skip_deserializing`
fn schema_files(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
schemas::typescript("Record<string, File>")
schemas::typescript("Record<string, File>", true)
}

/// The name of the project's manifest file within the project directory
Expand Down
96 changes: 57 additions & 39 deletions rust/src/schemas.rs
Expand Up @@ -25,61 +25,79 @@ pub fn generator() -> SchemaGenerator {
}

/// Generate the JSON Schema for a property with a specified TypeScript type.
pub fn typescript(typescript_type: &str) -> Schema {
pub fn typescript(typescript_type: &str, required: bool) -> Schema {
let mut extensions = Map::new();
extensions.insert("tsType".to_string(), json!(typescript_type));
extensions.insert("isRequired".to_string(), json!(required));
Schema::Object(SchemaObject {
extensions,
..Default::default()
})
}

/// Generate a JSON Schema for a type using the generator
pub fn generate<Type>() -> Result<JsonValue>
where
Type: JsonSchema,
{
let schema = generator().into_root_schema_for::<Type>();
let schema = serde_json::to_value(schema)?;

// Modify `$id`, `title` and `description` for compatibility with TypeScript
// and UI form generation.
// See https://github.com/stencila/stencila/pull/929#issuecomment-842623228
fn modify(value: JsonValue) -> JsonValue {
if let JsonValue::Object(object) = value {
let mut modified = serde_json::Map::<String, JsonValue>::new();
// Modify `$id`, `title` and `description` for compatibility with TypeScript
// and UI form generation. Also apply the `isRequired` override.
// See https://github.com/stencila/stencila/pull/929#issuecomment-842623228
fn transform(value: JsonValue) -> JsonValue {
if let JsonValue::Object(object) = value {
let mut modified = serde_json::Map::<String, JsonValue>::new();

// Copy over modified child properties
for (key, child) in &object {
modified.insert(key.clone(), modify(child.clone()));
}
// Copy over modified child properties
for (key, child) in &object {
modified.insert(key.clone(), transform(child.clone()));
}

// For `type:object` schemas, including sub-schemas..
if let Some(value) = object.get("type") {
if value == &serde_json::to_value("object").unwrap() {
// Put any `title` into `$id`
if let Some(title) = object.get("title") {
modified.insert("$id".into(), title.clone());
// For `type:object` schemas, including sub-schemas..
if let Some(value) = object.get("type") {
if value == &serde_json::to_value("object").unwrap() {
// Put any `title` into `$id`
if let Some(title) = object.get("title") {
modified.insert("$id".into(), title.clone());
}
// Parse any `description` and if multi-line, put
// the first "paragraph" into the `title`
if let Some(JsonValue::String(description)) = object.get("description") {
let paras = description.split("\n\n").collect::<Vec<&str>>();
if paras.len() > 1 {
modified.insert("title".into(), JsonValue::String(paras[0].into()));
modified.insert(
"description".into(),
JsonValue::String(paras[1..].join("\n\n")),
);
}
// Parse any `description` and if multi-line, put
// the first "paragraph" into the `title`
if let Some(JsonValue::String(description)) = object.get("description") {
let paras = description.split("\n\n").collect::<Vec<&str>>();
if paras.len() > 1 {
modified.insert("title".into(), JsonValue::String(paras[0].into()));
modified.insert(
"description".into(),
JsonValue::String(paras[1..].join("\n\n")),
);
}
// Check if any properties declare themselves `isRequired`
if let Some(JsonValue::Object(properties)) = object.get("properties") {
for (name, subschema) in properties {
if let Some(JsonValue::Bool(is_required)) = subschema.get("isRequired") {
let name = JsonValue::String(name.clone());
if let Some(JsonValue::Array(required)) = modified.get_mut("required") {
if *is_required {
required.push(name)
} else {
required.retain(|prop| *prop != name)
}
} else if *is_required {
modified.insert("required".into(), JsonValue::Array(vec![name]));
}
}
}
}
}
JsonValue::Object(modified)
} else {
value
}
JsonValue::Object(modified)
} else {
value
}
let schema = modify(schema);
}

/// Generate a JSON Schema for a type using the generator
pub fn generate<Type>() -> Result<JsonValue>
where
Type: JsonSchema,
{
let schema = generator().into_root_schema_for::<Type>();
let schema = serde_json::to_value(schema)?;
let schema = transform(schema);
Ok(schema)
}

0 comments on commit ac24ef7

Please sign in to comment.