Skip to content

Commit

Permalink
fix(Rust): Improve primitive types
Browse files Browse the repository at this point in the history
This fixes a bug associated with having `Null` be a `serde_json::Value`
in which it was consuming all primitive tpes on deserialization.

This also improves the typings for primitive (non-`Entity`) nodes,
including making `Object` and `Array` recursive and increasing the
precision of `Integer` and `Number`.
  • Loading branch information
nokome committed Jun 1, 2021
1 parent 37ca7e5 commit 98296aa
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 14 deletions.
7 changes: 7 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ serde_json = "1.0.64"
serde_with = "1.9.1"

[dev-dependencies]
maplit = "1.0.2"
pretty_assertions = "0.7.1"
1 change: 1 addition & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod prelude;
pub use prelude::Primitive;

#[rustfmt::skip]
mod types;
Expand Down
39 changes: 33 additions & 6 deletions rust/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,42 @@ pub use defaults::Defaults;
pub use serde::{de, Deserialize, Deserializer, Serialize};
pub use serde_json::Value;
pub use serde_with::skip_serializing_none;
use std::collections::HashMap;
use std::collections::BTreeMap;
pub use std::sync::Arc;

pub type Null = Value;
/// The set of primitive (non-Entity) node types
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Primitive {
Null,
Bool(Bool),
Integer(Integer),
Number(Number),
String(String),
Object(Object),
Array(Array),
}

/// A boolean value
pub type Bool = bool;
pub type Integer = i32;
pub type Number = f32;
pub type Array = Vec<Value>;
pub type Object = HashMap<String, Value>;

/// An integer value
///
/// Uses `i64` for maximum precision.
pub type Integer = i64;

/// A floating point value (a.k.a real number)
///
/// Uses `i64` for maximum precision.
pub type Number = f64;

/// An array value (a.k.a. vector)
pub type Array = Vec<Primitive>;

/// An object value (a.k.a map, dictionary)
///
/// Uses `BTreeMap` to preserve order.
pub type Object = BTreeMap<String, Primitive>;

// Checks the `type` property during deserialization.
// See notes in TypesScript function `interfaceSchemaToEnum`
Expand Down
4 changes: 2 additions & 2 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4904,7 +4904,7 @@ pub enum InlineContent {
Subscript(Subscript),
Superscript(Superscript),
VideoObject(VideoObject),
Null(Null),
Null,
Bool(Bool),
Integer(Integer),
Number(Number),
Expand Down Expand Up @@ -4949,7 +4949,7 @@ pub enum MediaObjectTypes {
#[serde(untagged)]
pub enum Node {
Entity(Entity),
Null(Null),
Null,
Bool(Bool),
Integer(Integer),
Number(Number),
Expand Down
109 changes: 105 additions & 4 deletions rust/tests/test.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,114 @@
//! These tests are intentionally simple and just test that
//! node types have expected traits e.g. `Clone`, `Serialize` etc.

use maplit::btreemap;
use pretty_assertions::assert_eq;
use serde_json::{json, Result, Value};
use stencila_schema::{
Article, BlockContent, CodeExpression, CreativeWorkAuthors, CreativeWorkTitle, InlineContent,
Paragraph, Person,
Paragraph, Person, Primitive,
};

#[test]
fn primitives_deserialize() -> Result<()> {
let null: Primitive = serde_json::from_str("null")?;
assert!(matches!(null, Primitive::Null));

let bool: Primitive = serde_json::from_str("true")?;
assert!(matches!(bool, Primitive::Bool(_)));

let bool: Primitive = serde_json::from_str("false")?;
assert!(matches!(bool, Primitive::Bool(_)));

let integer: Primitive = serde_json::from_str("42")?;
assert!(matches!(integer, Primitive::Integer(_)));

let number: Primitive = serde_json::from_str("3.14")?;
assert!(matches!(number, Primitive::Number(_)));

let string: Primitive = serde_json::from_str("\"str ing\"")?;
assert!(matches!(string, Primitive::String(_)));

let array: Primitive = serde_json::from_str(r#"[null, false, 42, 3.14, "string"]"#)?;
if let Primitive::Array(array) = array {
assert!(matches!(array[0], Primitive::Null));
assert!(matches!(array[1], Primitive::Bool(false)));
assert!(matches!(array[2], Primitive::Integer(_)));
assert!(matches!(array[3], Primitive::Number(_)));
assert!(matches!(array[4], Primitive::String(_)));
} else {
panic!("Not an array!")
};

let object: Primitive = serde_json::from_str(
r#"{
"a": null,
"b": false,
"c": 42,
"d": 3.14,
"e": "string"
}"#,
)?;
if let Primitive::Object(object) = object {
assert!(matches!(object["a"], Primitive::Null));
assert!(matches!(object["b"], Primitive::Bool(false)));
assert!(matches!(object["c"], Primitive::Integer(_)));
assert!(matches!(object["d"], Primitive::Number(_)));
assert!(matches!(object["e"], Primitive::String(_)));
} else {
panic!("Not an object!")
};

Ok(())
}

#[test]
fn primitives_serialize() -> Result<()> {
let null = Primitive::Null;
assert_eq!(serde_json::to_string(&null)?, "null");

let bool = Primitive::Bool(true);
assert_eq!(serde_json::to_string(&bool)?, "true");

let bool = Primitive::Bool(false);
assert_eq!(serde_json::to_string(&bool)?, "false");

let integer = Primitive::Integer(42);
assert_eq!(serde_json::to_string(&integer)?, "42");

let number = Primitive::Number(3.14);
assert_eq!(serde_json::to_string(&number)?, "3.14");

let string = Primitive::String("string".to_string());
assert_eq!(serde_json::to_string(&string)?, "\"string\"");

let array = Primitive::Array(vec![
Primitive::Null,
Primitive::Bool(false),
Primitive::Integer(42),
Primitive::Number(3.14),
Primitive::String("string".to_string()),
]);
assert_eq!(
serde_json::to_string(&array)?,
"[null,false,42,3.14,\"string\"]"
);

let object = Primitive::Object(btreemap! {
"a".to_string() => Primitive::Null,
"b".to_string() => Primitive::Bool(false),
"c".to_string() => Primitive::Integer(42),
"d".to_string() => Primitive::Number(3.14),
"e".to_string() => Primitive::String("string".to_string())
});
assert_eq!(
serde_json::to_string(&object)?,
r#"{"a":null,"b":false,"c":42,"d":3.14,"e":"string"}"#
);

Ok(())
}

fn article_fixture() -> Article {
Article {
title: Some(CreativeWorkTitle::String("The article title".into())),
Expand All @@ -34,20 +135,20 @@ fn article_fixture() -> Article {
}

#[test]
fn is_clonable() {
fn entity_is_clonable() {
let article1 = article_fixture();
let _article2 = article1.clone();
}

#[test]
fn is_debugable() {
fn entity_is_debuggable() {
let article = article_fixture();

assert!(format!("{:?}", article).starts_with("Article {"))
}

#[test]
fn is_serdeable() -> Result<()> {
fn entity_is_serdeable() -> Result<()> {
let article = article_fixture();
let json = json!({
"type": "Article",
Expand Down
4 changes: 2 additions & 2 deletions ts/bindings/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export function unionSchemaToEnum(
const variants = anyOf
?.map((schema) => {
const name = schemaToType(schema, context)
return ` ${name}(${name}),\n`
return name === 'Null' ? ` ${name},\n` : ` ${name}(${name}),\n`
})
.join('')

Expand Down Expand Up @@ -288,7 +288,7 @@ function anyOfToEnum(anyOf: JsonSchema[], context: Context): string {
.map((schema) => {
const type = schemaToType(schema, context)
const name = type.replace('<', '').replace('>', '')
return ` ${name}(${type}),\n`
return type === 'Null' ? name : ` ${name}(${type}),\n`
})
.join('')

Expand Down

0 comments on commit 98296aa

Please sign in to comment.