mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-07 12:31:56 +00:00
Drawings: drawing.create and drawing.set refuse a misspelt setting instead of ignoring it (#3890) (#12625)
A script that calls drawing.create or drawing.set with a misspelt setting now gets an error naming the setting. Before, the call reported success and the setting was lost. The two commands now read their parameters through input schemas, like every other drawing command. - schema/drawing.rs gains DrawingCreateParams and DrawingSetParams. Each field's reader keeps the refusal words the hand readers used. - The hand readers for units, projection, updates and revision_numbering move into the schema. - The drawing's line_group reads through cmd::line_group_of, so it keeps its sheets' words. drawing.view.edit is unchanged. - The output types are their own slice. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,32 +15,33 @@ use solidcraft_doc::drawing::balloon::balloons_by_row;
|
||||
use solidcraft_doc::drawing::fields::{Component, Values};
|
||||
use solidcraft_doc::drawing::table::Kind;
|
||||
use solidcraft_doc::drawing::view::{MAX_VIEW_DEPTH, View, ViewKind};
|
||||
use solidcraft_doc::drawing::{self, Drawing, Source, Units, Updates, template};
|
||||
use solidcraft_doc::drawing::{self, Drawing, Source, template};
|
||||
use solidcraft_doc::drawing_view::ViewDirection;
|
||||
use solidcraft_doc::{Document, ids};
|
||||
use solidcraft_geom::cell::{Content, Projection};
|
||||
use solidcraft_geom::cell::Content;
|
||||
use solidcraft_geom::drawing_view::ProjectedSide;
|
||||
use solidcraft_geom::paper::line::LineGroup;
|
||||
use solidcraft_geom::revision::Numbering;
|
||||
use solidcraft_geom::title_block::CellKind;
|
||||
|
||||
use super::CommandSpec;
|
||||
use crate::params::{bad, str_};
|
||||
use crate::schema::drawing::{
|
||||
SheetTemplateChange, TemplateApplyParams, TemplateApplyResult, TemplateEntry, TemplateListParams, TemplateListResult, TemplateSaveParams,
|
||||
TemplateSaveResult,
|
||||
DrawingCreateParams, DrawingSetParams, SheetTemplateChange, TemplateApplyParams, TemplateApplyResult, TemplateEntry, TemplateListParams,
|
||||
TemplateListResult, TemplateSaveParams, TemplateSaveResult,
|
||||
};
|
||||
use crate::schema::{TypedContract, output_schema_for};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
/// The longest template file path taken, in bytes, as the sheet export's.
|
||||
const MAX_PATH_BYTES: usize = 4096;
|
||||
const CREATE_CONTRACT: TypedContract<DrawingCreateParams> = TypedContract::new();
|
||||
const SET_CONTRACT: TypedContract<DrawingSetParams> = TypedContract::new();
|
||||
const TEMPLATE_APPLY_CONTRACT: TypedContract<TemplateApplyParams> = TypedContract::new();
|
||||
const TEMPLATE_LIST_CONTRACT: TypedContract<TemplateListParams> = TypedContract::new();
|
||||
const TEMPLATE_SAVE_CONTRACT: TypedContract<TemplateSaveParams> = TypedContract::new();
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("drawing.create", "Create Drawing", create).any_part().icon("new").doc_only().params(
|
||||
CommandSpec::new("drawing.create", "Create Drawing", crate::checked!("drawing.create", create)).any_part().icon("new").doc_only().contract(&CREATE_CONTRACT).params(
|
||||
"name?: the drawing's name (default: the design's); template?: a built-in's name (drawing.template.list; default iso-a3-part for a part, iso-a2-assembly for an assembly, a design that places components) or a .json template file (drawing.template.save); language?: the drawing's content language, a BCP 47 tag (default: the template's, en for the built-ins); units?: \"follow_model\" or one length unit such as \"mm\" (default: the template's); projection?: first or third (angle) (default: the template's, third for the built-ins); font?: the font family its text is set in, by name (default: the template's, the built-in font for the built-ins); line_group?: the ISO 128 line group its lines are drawn in, by its wide line, one of 0.25, 0.35, 0.5, 0.7, 1, 1.4 or 2 (default: the template's, 0.5 for the built-ins). Adds a drawing of this design made from the template, with ids of its own, and opens it (one undo step of the model) → as drawing.inspect",
|
||||
),
|
||||
CommandSpec::new("drawing.template.apply", "Apply Drawing Template", crate::checked!("drawing.template.apply", template_apply))
|
||||
@@ -96,10 +97,11 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("drawing.inspect", "Inspect Drawing", inspect).in_drawing().icon("inspect").noundo().doc_only().params(
|
||||
"→ {id, name, units, projection, updates: automatic or deferred, language, font, line_group, font_note: why its text is set in the built-in font instead (the font isn't on this computer, or can't be set on paper), or null, properties, sources: [{id}], sheets: [{id, name, size_mm, orientation, title_block (its definition, or null), title_block_cells: [{name, text, value?}] (each text cell as the sheet shows it: its fields' values, \"-\" for one with none, or the text typed over it with the value it hides as `value`), title_block_overrides, dpi, line_group (its own, or null for the drawing's), documents, excluded, views: [{id, kind: base|projected, parent?, side?, label, dimensions: [ids], balloons: [ids]}], base views by id, each followed by those projected from it}], revisions: {rows: [{row, revision, description, designer, checker, date, hidden}], the oldest first (the newest is the drawing's revision), next: the revision a new row gets}}. Read-only.",
|
||||
),
|
||||
CommandSpec::new("drawing.set", "Drawing Settings", set)
|
||||
CommandSpec::new("drawing.set", "Drawing Settings", crate::checked!("drawing.set", set))
|
||||
.in_drawing()
|
||||
.icon("settings")
|
||||
.doc_only()
|
||||
.contract(&SET_CONTRACT)
|
||||
.params("name?, language?: a BCP 47 tag, units?: \"follow_model\" or one length unit such as \"mm\", projection?: first or third (angle), which every projected view follows, updates?: automatic, or deferred: its views follow a changed model only on drawing.update, font?: the font family its text is set in, by name, never a file (\"\": the built-in font). line_group?: the ISO 128 line group its lines are drawn in, by its wide line: 0.25, 0.35, 0.5, 0.7, 1, 1.4 or 2; a sheet or a view may choose another. revision_numbering?: {start: 0 or 1}, the number new revisions count from: rows already there stay as typed. One undo step."),
|
||||
CommandSpec::new("drawing.properties", "Drawing Properties", properties).in_drawing().icon("settings").noundo().doc_only().params(
|
||||
"→ metadata: the drawing's properties as saved (other tools' keys too); properties: project, part_number, revision, author, organization, description and license, each its text or null. The design properties' rules (document.metadata).",
|
||||
@@ -114,55 +116,25 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.params("key, or keys: [keys] → metadata, removed: [keys that were there]"),
|
||||
];
|
||||
|
||||
/// `units` as given: `follow_model`, or a length unit the drawing's rules check.
|
||||
fn units(cmd: &str, p: &Value) -> Result<Option<Units>> {
|
||||
match p.get("units") {
|
||||
None => Ok(None),
|
||||
Some(Value::String(u)) if u == "follow_model" => Ok(Some(Units::FollowModel)),
|
||||
Some(Value::String(u)) => Ok(Some(Units::Fixed { unit: u.clone() })),
|
||||
Some(_) => Err(bad(cmd, "`units` is \"follow_model\" or a length unit such as \"mm\"")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `projection` as given: first or third angle.
|
||||
fn projection(cmd: &str, p: &Value) -> Result<Option<Projection>> {
|
||||
match p.get("projection") {
|
||||
None => Ok(None),
|
||||
Some(v) => serde_json::from_value(v.clone()).map(Some).map_err(|_| bad(cmd, "`projection` is first or third (angle)")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `updates` as given: when the views follow a changed model.
|
||||
fn updates(cmd: &str, p: &Value) -> Result<Option<Updates>> {
|
||||
match p.get("updates") {
|
||||
None => Ok(None),
|
||||
Some(v) => serde_json::from_value(v.clone()).map(Some).map_err(|_| bad(cmd, "`updates` is automatic or deferred")),
|
||||
}
|
||||
}
|
||||
|
||||
/// A `line_group` as given, for a drawing, a sheet or a view: one of ISO 128's groups, by its wide line.
|
||||
pub(crate) fn line_group_of(v: &Value) -> std::result::Result<LineGroup, String> {
|
||||
serde_json::from_value(v.clone()).map_err(|_| format!("`line_group` is one of {}", LineGroup::NAMES.join(", ")))
|
||||
}
|
||||
|
||||
/// [`line_group_of`], refused as `cmd`.
|
||||
pub(super) fn line_group(cmd: &str, v: &Value) -> Result<LineGroup> {
|
||||
serde_json::from_value(v.clone()).map_err(|_| bad(cmd, format!("`line_group` is one of {}", LineGroup::NAMES.join(", "))))
|
||||
line_group_of(v).map_err(|e| bad(cmd, e))
|
||||
}
|
||||
|
||||
/// `font` as given: a font family's name, never a file (drawings name fonts by family, so they
|
||||
/// travel).
|
||||
fn font<'a>(cmd: &str, p: &'a Value) -> Result<Option<&'a str>> {
|
||||
let font = text(cmd, p, "font")?;
|
||||
fn font<'a>(cmd: &str, font: Option<&'a str>) -> Result<Option<&'a str>> {
|
||||
if let Some(family) = font {
|
||||
solidcraft_sketch::check_family(family).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
}
|
||||
Ok(font.map(str::trim))
|
||||
}
|
||||
|
||||
/// How `p` asks new revisions to be named. A start the record's rules refuse is refused with them.
|
||||
fn revision_numbering(cmd: &str, p: &Value) -> Result<Option<Numbering>> {
|
||||
match p.get("revision_numbering") {
|
||||
None => Ok(None),
|
||||
Some(v) => serde_json::from_value(v.clone()).map(Some).map_err(|_| bad(cmd, "`revision_numbering` is {start: 0 or 1}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// An optional text parameter: absent, or a string.
|
||||
fn text<'a>(cmd: &str, p: &'a Value, key: &str) -> Result<Option<&'a str>> {
|
||||
match p.get(key) {
|
||||
@@ -183,29 +155,30 @@ fn create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(cmd, "the design is empty: there is nothing to draw yet"));
|
||||
}
|
||||
drawing::check_count(s.doc.drawings.len() + 1).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
let name = text(cmd, p, "name")?.map_or_else(|| s.doc.name.clone(), str::to_string);
|
||||
let request: DrawingCreateParams = crate::schema::read(cmd, p)?;
|
||||
let name = request.name.unwrap_or_else(|| s.doc.name.clone());
|
||||
let fail = |e: solidcraft_doc::DocError| bad(cmd, e.to_string());
|
||||
let from = text(cmd, p, "template")?.unwrap_or_else(|| template::default_for(template::is_assembly(&s.doc)));
|
||||
let from = request.template.as_deref().unwrap_or_else(|| template::default_for(template::is_assembly(&s.doc)));
|
||||
let chosen = template_named(cmd, from)?;
|
||||
// Not the id of a drawing undo or redo could bring back either.
|
||||
let steps = s.undo.iter().chain(&s.redo).filter_map(|step| step.part);
|
||||
let taken: BTreeSet<u64> = s.doc.drawings.iter().map(|d| d.id).chain(steps).collect();
|
||||
let mut d = template::instantiate(&chosen, &name, &taken, &mut s.ids).map_err(fail)?;
|
||||
if let Some(language) = text(cmd, p, "language")? {
|
||||
d.language = language.to_string();
|
||||
if let Some(language) = request.language {
|
||||
d.language = language;
|
||||
}
|
||||
if let Some(projection) = projection(cmd, p)? {
|
||||
if let Some(projection) = request.projection {
|
||||
d.projection = projection;
|
||||
}
|
||||
if let Some(group) = p.get("line_group") {
|
||||
d.line_group = line_group(cmd, group)?;
|
||||
if let Some(group) = request.line_group {
|
||||
d.line_group = group;
|
||||
}
|
||||
let source = d.allocate_id(&mut s.ids).map_err(fail)?;
|
||||
d.sources.push(Source { id: source });
|
||||
if let Some(units) = units(cmd, p)? {
|
||||
if let Some(units) = request.units {
|
||||
d.units = units;
|
||||
}
|
||||
if let Some(font) = font(cmd, p)? {
|
||||
if let Some(font) = font(cmd, request.font.as_deref())? {
|
||||
d.font = font.to_string();
|
||||
}
|
||||
d.validate().map_err(fail)?;
|
||||
@@ -500,29 +473,30 @@ fn format_mass(grams: f64) -> String {
|
||||
|
||||
fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "drawing.set";
|
||||
let request: DrawingSetParams = crate::schema::read(cmd, p)?;
|
||||
s.edit_drawing(cmd, |d| {
|
||||
if let Some(name) = text(cmd, p, "name")? {
|
||||
d.name = name.to_string();
|
||||
if let Some(name) = request.name {
|
||||
d.name = name;
|
||||
}
|
||||
if let Some(language) = text(cmd, p, "language")? {
|
||||
d.language = language.to_string();
|
||||
if let Some(language) = request.language {
|
||||
d.language = language;
|
||||
}
|
||||
if let Some(units) = units(cmd, p)? {
|
||||
if let Some(units) = request.units {
|
||||
d.units = units;
|
||||
}
|
||||
if let Some(projection) = projection(cmd, p)? {
|
||||
if let Some(projection) = request.projection {
|
||||
d.projection = projection;
|
||||
}
|
||||
if let Some(updates) = updates(cmd, p)? {
|
||||
if let Some(updates) = request.updates {
|
||||
d.updates = updates;
|
||||
}
|
||||
if let Some(group) = p.get("line_group") {
|
||||
d.line_group = line_group(cmd, group)?;
|
||||
if let Some(group) = request.line_group {
|
||||
d.line_group = group;
|
||||
}
|
||||
if let Some(font) = font(cmd, p)? {
|
||||
if let Some(font) = font(cmd, request.font.as_deref())? {
|
||||
d.font = font.to_string();
|
||||
}
|
||||
if let Some(numbering) = revision_numbering(cmd, p)? {
|
||||
if let Some(numbering) = request.revision_numbering {
|
||||
Arc::make_mut(&mut d.revisions).numbering = numbering;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::*;
|
||||
use crate::cmd::{Part, command_specs, find_command};
|
||||
use crate::tests::Folder;
|
||||
use solidcraft_doc::drawing::template;
|
||||
use solidcraft_doc::drawing::{Units, Updates, template};
|
||||
use solidcraft_geom::cell::Projection;
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
s.execute(id, &p).unwrap_or_else(|e| panic!("{id}: {e}"))
|
||||
@@ -263,6 +264,42 @@ fn a_drawing_is_made_only_of_a_design_with_something_in_it() {
|
||||
assert!(Arc::ptr_eq(&s.doc, &full));
|
||||
}
|
||||
|
||||
/// `drawing.create` and `drawing.set` refuse a key they don't take, as every other drawing command
|
||||
/// does, naming it, with nothing changed: a typo is never taken for a setting left as it was. Every
|
||||
/// key they take still takes, in one call.
|
||||
#[test]
|
||||
fn an_unknown_key_is_refused_and_changes_nothing() {
|
||||
let mut s = design();
|
||||
let before = s.doc.clone();
|
||||
for (p, key) in [(json!({"nmae": "Plate"}), "nmae"), (json!({"name": "Plate", "profile": "iso_129_1"}), "profile")] {
|
||||
let e = refused(&mut s, "drawing.create", p.clone());
|
||||
assert!(e.contains(&format!("unknown field `{key}`")), "{p}: {e}");
|
||||
assert!(Arc::ptr_eq(&s.doc, &before) && s.open_drawing.is_none(), "{p}: nothing changed");
|
||||
}
|
||||
let every =
|
||||
json!({"name": "Plate", "template": "iso-a4-part", "language": "de", "units": "in", "projection": "first", "line_group": "0.7", "font": ""});
|
||||
run(&mut s, "drawing.create", every);
|
||||
let made = open_one(&s);
|
||||
assert_eq!((made.name.as_str(), made.language.as_str(), made.projection, made.line_group), ("Plate", "de", Projection::First, LineGroup::Mm0_7));
|
||||
let paper = made.sheets[0].sheet.size_mm().unwrap();
|
||||
assert_eq!((&made.units, paper.x.max(paper.y)), (&Units::Fixed { unit: "in".into() }, 297.0), "A4, from the template");
|
||||
let steps = s.undo.len();
|
||||
for (p, key) in [(json!({"line_groups": "0.5"}), "line_groups"), (json!({"font": "Liberation Sans", "fnot": 1}), "fnot")] {
|
||||
let e = refused(&mut s, "drawing.set", p.clone());
|
||||
assert!(e.contains(&format!("unknown field `{key}`")), "{p}: {e}");
|
||||
assert!(Arc::ptr_eq(&open_one(&s), &made) && s.undo.len() == steps, "{p}: nothing changed");
|
||||
}
|
||||
let every = json!({"name": "Plate 2", "language": "en-GB", "units": "follow_model", "projection": "third", "updates": "deferred", "line_group": "1", "font": "Liberation Sans", "revision_numbering": {"start": 0}});
|
||||
run(&mut s, "drawing.set", every);
|
||||
let set = open_one(&s);
|
||||
assert_eq!((set.name.as_str(), set.language.as_str(), &set.units, set.projection), ("Plate 2", "en-GB", &Units::FollowModel, Projection::Third));
|
||||
assert_eq!(
|
||||
(set.updates, set.line_group, set.font.as_str(), set.revisions.numbering.start),
|
||||
(Updates::Deferred, LineGroup::Mm1, "Liberation Sans", 0)
|
||||
);
|
||||
assert_eq!(s.undo.len(), steps + 1, "one undo step");
|
||||
}
|
||||
|
||||
/// While a drawing is open the model's commands wait, and the drawing's run only while one is:
|
||||
/// running a command, running it inside another and listing it all say so.
|
||||
#[test]
|
||||
@@ -650,6 +687,8 @@ fn hostile_drawing_params_never_panic() {
|
||||
run(&mut s, "drawing.create", json!({"name": "Second"}));
|
||||
assert_eq!(s.scratch().open_drawing, s.open_drawing, "each copy has the drawing open");
|
||||
let keys = ["name", "language", "units", "drawing", "key", "value", "values", "keys", "checkpoint", "id"];
|
||||
// `drawing.create`'s and `drawing.set`'s other keys.
|
||||
let keys = [&keys[..], &["template", "projection", "updates", "line_group", "font", "revision_numbering"]].concat();
|
||||
let skipped = |id: &str| matches!(id, "doc.open" | "file.save" | "file.save_as") || id.starts_with("doc.recover");
|
||||
let mut internal = Vec::new();
|
||||
for spec in command_specs().iter().filter(|spec| spec.part != Part::Model && !skipped(spec.id)) {
|
||||
|
||||
@@ -55,6 +55,7 @@ mod join_pieces;
|
||||
pub(crate) use bom::{SAME_AS_COMPONENT, occurrence_bom_type, record_item_numbers};
|
||||
pub(crate) use component::{path_ids, refuse_filled_virtual};
|
||||
pub(crate) use construct::{GeoForm, GeoFormError, REF as GEO_REF_FORMS, SKETCH_PART, geo_form, is_model_axis, plane_axis_point_kind};
|
||||
pub(crate) use drawing::line_group_of;
|
||||
pub use drawing_ui::pending_icon as drawing_pending_icon;
|
||||
pub(crate) use face::{FaceRefForm, face_ref_form};
|
||||
pub(crate) use features::profiles as feature_profiles;
|
||||
|
||||
@@ -1,8 +1,142 @@
|
||||
//! Contracts and result types for the commands in `cmd/drawing.rs` (#3875): the templates a drawing
|
||||
//! starts from.
|
||||
//! Contracts and result types for the commands in `cmd/drawing.rs` (#3875): making a drawing,
|
||||
//! its own settings, and the templates a drawing starts from.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::Value;
|
||||
use solidcraft_doc::drawing::{Units, Updates};
|
||||
use solidcraft_geom::cell::Projection;
|
||||
use solidcraft_geom::paper::line::LineGroup;
|
||||
use solidcraft_geom::revision::Numbering;
|
||||
|
||||
use super::check::whole;
|
||||
use super::drawing_view::LineGroupParam;
|
||||
use super::types::present;
|
||||
|
||||
/// Make a drawing of the design from a template, with ids of its own, and open it.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct DrawingCreateParams {
|
||||
/// The drawing's name (the design's when not given).
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "String")]
|
||||
pub name: Option<String>,
|
||||
/// A built-in's name (`drawing.template.list`) or a .json template file
|
||||
/// (`drawing.template.save`): the design's kind's built-in when not given.
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "String")]
|
||||
pub template: Option<String>,
|
||||
/// Its content language, a BCP 47 tag (the template's when not given).
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "String")]
|
||||
pub language: Option<String>,
|
||||
#[serde(default, deserialize_with = "units", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "units_schema")]
|
||||
pub units: Option<Units>,
|
||||
#[serde(default, deserialize_with = "projection", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "projection_schema")]
|
||||
pub projection: Option<Projection>,
|
||||
/// The ISO 128 line group its lines are drawn in (the template's when not given).
|
||||
#[serde(default, deserialize_with = "line_group", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "LineGroupParam")]
|
||||
pub line_group: Option<LineGroup>,
|
||||
/// The font family its text is set in, by name, never a file ("": the built-in font).
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "String")]
|
||||
pub font: Option<String>,
|
||||
}
|
||||
|
||||
/// Change the open drawing's own settings: one undo step.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct DrawingSetParams {
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "String")]
|
||||
pub name: Option<String>,
|
||||
/// Its content language, a BCP 47 tag.
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "String")]
|
||||
pub language: Option<String>,
|
||||
#[serde(default, deserialize_with = "units", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "units_schema")]
|
||||
pub units: Option<Units>,
|
||||
/// Which every projected view follows.
|
||||
#[serde(default, deserialize_with = "projection", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "projection_schema")]
|
||||
pub projection: Option<Projection>,
|
||||
#[serde(default, deserialize_with = "updates", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "updates_schema")]
|
||||
pub updates: Option<Updates>,
|
||||
/// The ISO 128 line group its lines are drawn in; a sheet or a view may choose another.
|
||||
#[serde(default, deserialize_with = "line_group", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "LineGroupParam")]
|
||||
pub line_group: Option<LineGroup>,
|
||||
/// The font family its text is set in, by name, never a file ("": the built-in font).
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "String")]
|
||||
pub font: Option<String>,
|
||||
#[serde(default, deserialize_with = "revision_numbering", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "revision_numbering_schema")]
|
||||
pub revision_numbering: Option<Numbering>,
|
||||
}
|
||||
|
||||
/// `units` as given: `follow_model`, or a length unit the drawing's rules check.
|
||||
fn units<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Units>, D::Error> {
|
||||
match Value::deserialize(d)? {
|
||||
Value::String(u) if u == "follow_model" => Ok(Some(Units::FollowModel)),
|
||||
Value::String(unit) => Ok(Some(Units::Fixed { unit })),
|
||||
_ => Err(D::Error::custom(whole("`units` is \"follow_model\" or a length unit such as \"mm\""))),
|
||||
}
|
||||
}
|
||||
|
||||
fn units_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"description": "\"follow_model\" (the model's unit), or one length unit such as \"mm\".", "type": "string"})
|
||||
}
|
||||
|
||||
/// A `line_group` as given, in the words a sheet's is refused with.
|
||||
fn line_group<'de, D: Deserializer<'de>>(d: D) -> Result<Option<LineGroup>, D::Error> {
|
||||
crate::cmd::line_group_of(&Value::deserialize(d)?).map(Some).map_err(|e| D::Error::custom(whole(e)))
|
||||
}
|
||||
|
||||
/// `projection` as given: first or third angle.
|
||||
fn projection<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Projection>, D::Error> {
|
||||
serde_json::from_value(Value::deserialize(d)?).map(Some).map_err(|_| D::Error::custom(whole("`projection` is first or third (angle)")))
|
||||
}
|
||||
|
||||
fn projection_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"description": "First or third angle projection.", "type": "string", "enum": ["first", "third"]})
|
||||
}
|
||||
|
||||
/// `updates` as given: when the views follow a changed model.
|
||||
fn updates<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Updates>, D::Error> {
|
||||
serde_json::from_value(Value::deserialize(d)?).map(Some).map_err(|_| D::Error::custom(whole("`updates` is automatic or deferred")))
|
||||
}
|
||||
|
||||
fn updates_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"description": "When its views follow a changed model: automatic, or deferred (only on drawing.update).",
|
||||
"type": "string",
|
||||
"enum": ["automatic", "deferred"]
|
||||
})
|
||||
}
|
||||
|
||||
/// How new revisions are to be numbered. A start the record's rules refuse is refused with them.
|
||||
fn revision_numbering<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Numbering>, D::Error> {
|
||||
serde_json::from_value(Value::deserialize(d)?).map(Some).map_err(|_| D::Error::custom(whole("`revision_numbering` is {start: 0 or 1}")))
|
||||
}
|
||||
|
||||
fn revision_numbering_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"description": "The number new revisions count from: rows already there stay as typed.",
|
||||
"type": "object",
|
||||
"properties": {"start": {"type": "integer", "enum": [0, 1]}},
|
||||
"required": ["start"],
|
||||
"additionalProperties": false
|
||||
})
|
||||
}
|
||||
|
||||
/// Give each sheet of the drawing open the paper, drawing border and title block of a template's
|
||||
/// sheet, moving nothing.
|
||||
|
||||
@@ -6,6 +6,10 @@ use crate::schema::{ParamCheck, input_schema_for, output_schema_for};
|
||||
|
||||
#[test]
|
||||
fn schemas_and_the_key_check_agree() {
|
||||
let create = ["font", "language", "line_group", "name", "projection", "template", "units"];
|
||||
assert_keys(input_schema_for::<DrawingCreateParams>(), &create);
|
||||
let set = ["font", "language", "line_group", "name", "projection", "revision_numbering", "units", "updates"];
|
||||
assert_keys(input_schema_for::<DrawingSetParams>(), &set);
|
||||
assert_keys(input_schema_for::<TemplateListParams>(), &[]);
|
||||
assert_keys(input_schema_for::<TemplateApplyParams>(), &["dry_run", "template"]);
|
||||
assert_keys(input_schema_for::<TemplateSaveParams>(), &["path"]);
|
||||
@@ -31,14 +35,57 @@ fn the_template_save_reply_matches_its_output_schema() {
|
||||
/// A call that already fails gets the same error in strict as with the check off.
|
||||
#[test]
|
||||
fn strict_keeps_todays_errors() {
|
||||
for p in [json!({"template": "iso-b2-part"}), json!({"template": ""})] {
|
||||
let error = |level| drawing_session(level).0.execute("drawing.template.apply", &p).err().map(|e| e.to_string());
|
||||
let calls = [
|
||||
("drawing.template.apply", json!({"template": "iso-b2-part"})),
|
||||
("drawing.template.apply", json!({"template": ""})),
|
||||
("drawing.create", json!({"units": 3})),
|
||||
("drawing.create", json!({"units": "parsec"})),
|
||||
("drawing.create", json!({"name": 7})),
|
||||
("drawing.create", json!({"projection": "second"})),
|
||||
("drawing.create", json!({"line_group": "0.6"})),
|
||||
("drawing.create", json!({"font": "a.ttf"})),
|
||||
("drawing.create", json!({"template": "iso-b2-part"})),
|
||||
("drawing.set", json!({"updates": "later"})),
|
||||
("drawing.set", json!({"line_group": null})),
|
||||
("drawing.set", json!({"name": null})),
|
||||
("drawing.set", json!({"language": "e"})),
|
||||
("drawing.set", json!({"revision_numbering": {"start": 2}})),
|
||||
("drawing.set", json!({"revision_numbering": {"begin": 1}})),
|
||||
];
|
||||
for (id, p) in calls {
|
||||
let error = |level| drawing_session(level).0.execute(id, &p).err().map(|e| e.to_string());
|
||||
let off = error(ParamCheck::Off);
|
||||
assert!(off.is_some(), "{p} should fail today");
|
||||
assert_eq!(error(ParamCheck::Strict), off, "{p}");
|
||||
assert!(off.is_some(), "{id} {p} should fail today");
|
||||
assert_eq!(error(ParamCheck::Strict), off, "{id} {p}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `updates` belongs to `drawing.set`: a new drawing takes its template's, so `drawing.create` refuses
|
||||
/// the key at every check level, adding no drawing and no undo step.
|
||||
#[test]
|
||||
fn create_refuses_the_set_only_updates_key() {
|
||||
for level in [ParamCheck::Off, ParamCheck::Strict] {
|
||||
let (mut s, _) = drawing_session(level);
|
||||
let (drawings, steps) = (s.doc.drawings.len(), s.undo.len());
|
||||
let e = s.execute("drawing.create", &json!({"updates": "deferred"})).unwrap_err().to_string();
|
||||
// Strict's contract check speaks first ("unknown key"), the schema reader otherwise ("unknown field").
|
||||
assert!(e.contains("unknown") && e.contains("`updates`"), "{level:?}: {e}");
|
||||
assert_eq!((s.doc.drawings.len(), s.undo.len()), (drawings, steps), "{level:?}: nothing changed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every key `drawing.create` and `drawing.set` take, in one call each: no warnings in warn mode, so
|
||||
/// the contracts take what the commands do.
|
||||
#[test]
|
||||
fn the_drawing_settings_contracts_take_every_key() {
|
||||
let (mut s, _) = drawing_session(ParamCheck::Warn);
|
||||
let set = json!({"name": "Plate 2", "language": "en-GB", "units": "in", "projection": "first", "updates": "deferred", "line_group": "0.7", "font": "", "revision_numbering": {"start": 0}});
|
||||
assert!(s.execute("drawing.set", &set).unwrap().get("param_warnings").is_none());
|
||||
s.execute("drawing.close", &json!({})).unwrap();
|
||||
let create = json!({"name": "Plate 3", "template": "iso-a4-part", "language": "de", "units": "follow_model", "projection": "third", "line_group": "1", "font": ""});
|
||||
assert!(s.execute("drawing.create", &create).unwrap().get("param_warnings").is_none());
|
||||
}
|
||||
|
||||
/// Applying a template, as a dry run and for real, matches the output schema with no warnings in
|
||||
/// warn mode.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user