mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 21:51:36 +00:00
Commands: a schema with no required parameter allows null params
The engine reads null params as {}, but engine.commands listed every
schema with the root type "object", so a client that validated a call
against the schema refused one the engine accepts. A schema with no
required parameter now has the root type ["object", "null"]. A schema
with a required parameter keeps "object", because the engine refuses
null params for it as a missing parameter. Nested objects are unchanged.
This commit is contained in:
@@ -273,11 +273,15 @@ impl ParamSchema {
|
||||
/// The schema as a JSON Schema 2020-12 object schema. Defaults written as JSON go in
|
||||
/// `default`, a parameter that accepts null as omitted is an `anyOf` with `{"type": "null"}`,
|
||||
/// and VectorCraft's own fields are `x-since`, `x-deprecatedSince`, `x-lenient`, `x-unit`,
|
||||
/// `x-kind`, `x-cost`, `x-writes` and `x-returns`. A JSON Schema validator accepts the values
|
||||
/// [`ParamSchema::validate`] accepts, except that an `x-lenient` parameter's schema states only
|
||||
/// the documented type.
|
||||
/// `x-kind`, `x-cost`, `x-writes` and `x-returns`. The root `type` is `["object", "null"]` when
|
||||
/// no parameter is required, because null params read as `{}`, and `"object"` otherwise. A JSON
|
||||
/// Schema validator accepts the values [`ParamSchema::validate`] accepts, except that an
|
||||
/// `x-lenient` parameter's schema states only the documented type.
|
||||
pub fn to_json(&self) -> Value {
|
||||
let mut o = object_schema(self.params, 0);
|
||||
if !self.params.iter().any(|p| p.required) {
|
||||
o.insert("type".into(), json!(["object", "null"]));
|
||||
}
|
||||
o.insert("$schema".into(), DIALECT.into());
|
||||
if !self.summary.is_empty() {
|
||||
o.insert("description".into(), self.summary.into());
|
||||
|
||||
@@ -501,6 +501,28 @@ fn the_json_form_is_an_object_schema() {
|
||||
assert_eq!(range["properties"]["x"], json!({"type": "number", "exclusiveMinimum": 0.0, "maximum": 1.0, "x-since": 1}));
|
||||
}
|
||||
|
||||
/// Null params read as `{}`, so the root of the JSON form allows null exactly when `{}` passes.
|
||||
/// A nested object does not read null as `{}`.
|
||||
#[test]
|
||||
fn the_json_form_allows_null_params_when_no_parameter_is_required() {
|
||||
const OPTIONAL: &[Param] = &[Param::new("inner", Type::Object(&[Param::new("y", Type::Boolean).optional()])).optional()];
|
||||
let optional = ParamSchema::new(OPTIONAL, "");
|
||||
let written = optional.to_json();
|
||||
conformance::check_schema(&written, "#").unwrap();
|
||||
assert_eq!(written["type"], json!(["object", "null"]));
|
||||
assert_eq!(written["properties"]["inner"]["type"], "object");
|
||||
let required = one(Type::Boolean);
|
||||
assert_eq!(required.to_json()["type"], "object");
|
||||
for schema in [optional, required] {
|
||||
let written = schema.to_json();
|
||||
for params in [json!(null), json!({}), json!(5), json!([]), json!("x"), json!({"inner": null}), json!({"inner": {}})] {
|
||||
assert_eq!(schema.validate(¶ms).is_ok(), conformance::instance_valid(&written, ¶ms), "{params} against {written}");
|
||||
}
|
||||
}
|
||||
assert!(optional.validate(&json!(null)).is_ok());
|
||||
assert!(required.validate(&json!(null)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hash_follows_every_schema_but_not_their_order() {
|
||||
let a = ParamSchema::new(VIEW, "save a view");
|
||||
|
||||
@@ -29,7 +29,7 @@ use crate::{EngineError, Result};
|
||||
pub const API_VERSION: u32 = 1;
|
||||
|
||||
/// [`schema_hash`] at each API level, oldest first; the last row is [`API_VERSION`]'s.
|
||||
pub const SCHEMA_HASHES: &[(u32, u64)] = &[(1, 0xda5b_4c1e_fe4e_c697)];
|
||||
pub const SCHEMA_HASHES: &[(u32, u64)] = &[(1, 0x9cdb_1087_7074_855c)];
|
||||
|
||||
/// The hash of every built-in command's schema ([`vectorcraft_apischema::hash`]).
|
||||
pub fn schema_hash() -> u64 {
|
||||
@@ -151,6 +151,8 @@ mod tests {
|
||||
assert!(e.contains("view.saved.new") && e.contains("`zoom` must be a number > 0, not -1"), "{e}");
|
||||
let e = s.execute("view.saved.new", &json!({"center": [0, 0], "zoom": 1, "rotaton": 5})).unwrap_err().to_string();
|
||||
assert!(e.contains("unknown parameter `rotaton`; did you mean `rotation`?"), "{e}");
|
||||
let e = s.execute("view.saved.new", &Value::Null).unwrap_err().to_string();
|
||||
assert!(e.contains("view.saved.new") && e.contains("missing `center`"), "{e}");
|
||||
assert!(s.doc().unwrap().doc.views.is_empty());
|
||||
assert!(s.doc().unwrap().history.undo.is_empty());
|
||||
}
|
||||
@@ -285,6 +287,15 @@ mod tests {
|
||||
("view.transparencyGrid", json!({"on": 1})),
|
||||
("view.snapToGlyph.options", json!({"bounds": false, "angular": true})),
|
||||
("view.snapToGlyph.options", json!({"bounds": null})),
|
||||
("view.capsLock", json!(null)),
|
||||
("view.saved.new", json!(null)),
|
||||
("view.saved.edit", json!(null)),
|
||||
("view.saved.list", json!(null)),
|
||||
("view.transparencyGrid", json!(null)),
|
||||
("view.snapToGlyph.options", json!(null)),
|
||||
("view.capsLock", json!([])),
|
||||
("view.saved.new", json!(5)),
|
||||
("view.transparencyGrid", json!("on")),
|
||||
];
|
||||
assert_eq!(schemas().len(), 6);
|
||||
for (id, schema) in schemas() {
|
||||
|
||||
+3
-1
@@ -1392,7 +1392,9 @@ Some commands have a machine-readable parameter schema: at present `view.capsLoc
|
||||
fields are `x-since` (the API level that added the command or the parameter), `x-deprecatedSince`, `x-lenient`,
|
||||
`x-unit`, `x-kind` (`id`, `color` or `point`), `x-default` (a default the command works out, such as `toggle`),
|
||||
`x-cost`, `x-writes` (`nothing`, `session`, `preferences` or `document`) and `x-returns`. A parameter that reads null
|
||||
as left out is an `anyOf` with `{"type": "null"}`. A number with no fractional part, such as `2.0`, is an integer, as
|
||||
as left out is an `anyOf` with `{"type": "null"}`. Null params read as `{}`, so a schema with no required parameter has
|
||||
the root `type` `["object", "null"]`. A schema with a required parameter has the root `type` `"object"`, and the engine
|
||||
refuses null params for it as a missing parameter. A number with no fractional part, such as `2.0`, is an integer, as
|
||||
JSON Schema defines one, and the command receives `2`. An `x-lenient` parameter's schema states the documented type,
|
||||
and the engine also passes a value of another type to the command. The command's `params` text is written from the same
|
||||
schema.
|
||||
|
||||
Reference in New Issue
Block a user