Skip to main content

ifc_constraint/
authoring.rs

1//! Transaction-staged authoring for bounded constraints.
2//!
3//! Every writer binds the model's declared release and lays its record out
4//! by attribute name from that release's table (see `release.rs`, #212).
5//! What the release cannot hold is refused, never dropped or written into a
6//! slot that means something else. The rooted `IfcRelAssociatesConstraint`
7//! lives in `association.rs`.
8
9use std::collections::HashSet;
10use std::sync::Arc;
11
12use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
13use ifc_schema::{Schema, TypeKind};
14
15use crate::datetime::DateTimeInput;
16use crate::release::{bind, Layout};
17
18use crate::types::{
19    Benchmark, ConstraintGrade, LogicalOperator, MetricValueDraft, ObjectiveQualifier,
20};
21use crate::{ConstraintError, ConstraintResult};
22
23const METRIC: &str = "IFCMETRIC";
24const OBJECTIVE: &str = "IFCOBJECTIVE";
25const RESOURCE_REL: &str = "IFCRESOURCECONSTRAINTRELATIONSHIP";
26
27/// Common inherited `IfcConstraint` fields.
28#[derive(Debug, Clone, Copy)]
29#[non_exhaustive]
30pub struct ConstraintBaseDraft<'a> {
31    /// Required constraint name.
32    pub name: &'a str,
33    /// Optional description.
34    pub description: Option<&'a str>,
35    /// Typed constraint grade.
36    pub grade: ConstraintGrade,
37    /// Optional source label.
38    pub source: Option<&'a str>,
39    /// Optional existing or earlier-staged actor-select target.
40    pub creating_actor: Option<EntityId>,
41    /// Optional creation time: IFC4/IFC4X3 `IfcDateTime` text, or an
42    /// existing or earlier-staged IFC2X3 `IfcDateTimeSelect` record.
43    pub creation_time: Option<DateTimeInput<'a>>,
44    /// Required when `grade` is user-defined.
45    pub user_defined_grade: Option<&'a str>,
46}
47
48impl<'a> ConstraintBaseDraft<'a> {
49    /// Starts a draft from its required fields; every other field is unset.
50    #[must_use]
51    pub fn new(name: &'a str, grade: ConstraintGrade) -> Self {
52        Self {
53            name,
54            description: None,
55            grade,
56            source: None,
57            creating_actor: None,
58            creation_time: None,
59            user_defined_grade: None,
60        }
61    }
62
63    /// Sets `description`: Optional description.
64    #[must_use]
65    pub fn description(mut self, value: &'a str) -> Self {
66        self.description = Some(value);
67        self
68    }
69
70    /// Sets `source`: Optional source label.
71    #[must_use]
72    pub fn source(mut self, value: &'a str) -> Self {
73        self.source = Some(value);
74        self
75    }
76
77    /// Sets `creating_actor`: Optional existing or earlier-staged actor-select
78    /// target.
79    #[must_use]
80    pub fn creating_actor(mut self, value: EntityId) -> Self {
81        self.creating_actor = Some(value);
82        self
83    }
84
85    /// Sets `creation_time`: text, or an IFC2X3 date record.
86    #[must_use]
87    pub fn creation_time(mut self, value: impl Into<DateTimeInput<'a>>) -> Self {
88        self.creation_time = Some(value.into());
89        self
90    }
91
92    /// Sets `user_defined_grade`: Required when `grade` is user-defined.
93    #[must_use]
94    pub fn user_defined_grade(mut self, value: &'a str) -> Self {
95        self.user_defined_grade = Some(value);
96        self
97    }
98}
99
100/// Draft for one `IfcMetric`.
101#[derive(Debug, Clone, Copy)]
102#[non_exhaustive]
103pub struct MetricDraft<'a> {
104    /// Inherited constraint fields.
105    pub base: ConstraintBaseDraft<'a>,
106    /// Comparison benchmark.
107    pub benchmark: Benchmark,
108    /// Optional source label for the data value.
109    pub value_source: Option<&'a str>,
110    /// Optional preserved metric SELECT value.
111    pub data_value: Option<MetricValueDraft<'a>>,
112    /// Optional existing or earlier-staged `IfcReference`.
113    pub reference_path: Option<EntityId>,
114}
115
116impl<'a> MetricDraft<'a> {
117    /// Starts a draft from its required fields; every other field is unset.
118    #[must_use]
119    pub fn new(base: ConstraintBaseDraft<'a>, benchmark: Benchmark) -> Self {
120        Self {
121            base,
122            benchmark,
123            value_source: None,
124            data_value: None,
125            reference_path: None,
126        }
127    }
128
129    /// Sets `value_source`: Optional source label for the data value.
130    #[must_use]
131    pub fn value_source(mut self, value: &'a str) -> Self {
132        self.value_source = Some(value);
133        self
134    }
135
136    /// Sets `data_value`: Optional preserved metric SELECT value.
137    #[must_use]
138    pub fn data_value(mut self, value: MetricValueDraft<'a>) -> Self {
139        self.data_value = Some(value);
140        self
141    }
142
143    /// Sets `reference_path`: Optional existing or earlier-staged
144    /// `IfcReference`.
145    #[must_use]
146    pub fn reference_path(mut self, value: EntityId) -> Self {
147        self.reference_path = Some(value);
148        self
149    }
150}
151
152/// Draft for one `IfcObjective`.
153#[derive(Debug, Clone, Copy)]
154#[non_exhaustive]
155pub struct ObjectiveDraft<'a> {
156    /// Inherited constraint fields.
157    pub base: ConstraintBaseDraft<'a>,
158    /// Optional non-empty ordered constraints.
159    pub benchmark_values: Option<&'a [EntityId]>,
160    /// Optional logical operator.
161    pub logical_aggregator: Option<LogicalOperator>,
162    /// Objective purpose qualifier.
163    pub qualifier: ObjectiveQualifier,
164    /// Required when `qualifier` is user-defined.
165    pub user_defined_qualifier: Option<&'a str>,
166}
167
168impl<'a> ObjectiveDraft<'a> {
169    /// Starts a draft from its required fields; every other field is unset.
170    #[must_use]
171    pub fn new(base: ConstraintBaseDraft<'a>, qualifier: ObjectiveQualifier) -> Self {
172        Self {
173            base,
174            benchmark_values: None,
175            logical_aggregator: None,
176            qualifier,
177            user_defined_qualifier: None,
178        }
179    }
180
181    /// Sets `benchmark_values`: Optional non-empty ordered constraints.
182    #[must_use]
183    pub fn benchmark_values(mut self, value: &'a [EntityId]) -> Self {
184        self.benchmark_values = Some(value);
185        self
186    }
187
188    /// Sets `logical_aggregator`: Optional logical operator.
189    #[must_use]
190    pub fn logical_aggregator(mut self, value: LogicalOperator) -> Self {
191        self.logical_aggregator = Some(value);
192        self
193    }
194
195    /// Sets `user_defined_qualifier`: Required when `qualifier` is user-
196    /// defined.
197    #[must_use]
198    pub fn user_defined_qualifier(mut self, value: &'a str) -> Self {
199        self.user_defined_qualifier = Some(value);
200        self
201    }
202}
203
204/// Draft for one resource-level constraint relationship.
205#[derive(Debug, Clone, Copy)]
206#[non_exhaustive]
207pub struct ResourceConstraintDraft<'a> {
208    /// Optional relationship name.
209    pub name: Option<&'a str>,
210    /// Optional relationship description.
211    pub description: Option<&'a str>,
212    /// Existing or earlier-staged metric/objective.
213    pub relating_constraint: EntityId,
214    /// Non-empty unique resource-select targets.
215    pub related_resources: &'a [EntityId],
216}
217
218impl<'a> ResourceConstraintDraft<'a> {
219    /// Starts a draft from its required fields; every other field is unset.
220    #[must_use]
221    pub fn new(relating_constraint: EntityId, related_resources: &'a [EntityId]) -> Self {
222        Self {
223            name: None,
224            description: None,
225            relating_constraint,
226            related_resources,
227        }
228    }
229
230    /// Sets `name`: Optional relationship name.
231    #[must_use]
232    pub fn name(mut self, value: &'a str) -> Self {
233        self.name = Some(value);
234        self
235    }
236
237    /// Sets `description`: Optional relationship description.
238    #[must_use]
239    pub fn description(mut self, value: &'a str) -> Self {
240        self.description = Some(value);
241        self
242    }
243}
244
245/// Validate and stage one metric in the model's declared release.
246///
247/// # Release
248///
249/// IFC4 and IFC4X3 declare eleven attributes. IFC2X3 declares ten: no
250/// `ReferencePath`, a required `DataValue`, `CreationTime` as an
251/// `IfcDateTimeSelect` record, and an `IfcBenchmarkEnum` without IFC4's
252/// `INCLUDES`, `NOTINCLUDES`, `INCLUDEDIN` and `NOTINCLUDEDIN`.
253///
254/// # Errors
255///
256/// A user-defined grade without `UserDefinedGrade`; a reference outside its
257/// declared type in the release; a header binding no single verified
258/// release (`MultipleSchemas`, `UnsupportedSchema`); a value the release
259/// does not declare (`AuthoringNotInSchema`) or cannot hold
260/// (`AuthoringValueType`: a token its enumeration lacks, text where it
261/// declares a record); a data value type outside the release's
262/// `IfcMetricValueSelect` (`AuthoringInvalid`); and a required value left unset
263/// (`AuthoringRequired`). Nothing is staged on an error.
264pub fn create_metric(
265    tx: &mut Transaction,
266    model: &Model,
267    draft: MetricDraft<'_>,
268) -> ConstraintResult<EntityId> {
269    let layout = bind(model)?;
270    layout.require_entity(METRIC)?;
271    let base = base_values(layout, tx, model, METRIC, draft.base)?;
272    if let Some(path) = draft.reference_path {
273        if layout.declared(METRIC, "ReferencePath").is_some() {
274            validate_target_in(layout.schema(), tx, model, path, "IfcReference")?;
275        }
276    }
277    let data_value = match draft.data_value {
278        None => Value::Null,
279        Some(MetricValueDraft::Entity(target)) => {
280            validate_target_in(layout.schema(), tx, model, target, "IfcMetricValueSelect")?;
281            Value::Ref(target)
282        }
283        Some(MetricValueDraft::Typed { type_name, value }) => {
284            if !layout
285                .schema()
286                .accepts_type("IfcMetricValueSelect", type_name)
287            {
288                return Err(ConstraintError::AuthoringInvalid {
289                    entity: METRIC,
290                    attribute: "DataValue",
291                    value: format!(
292                        "type {type_name} is outside {:?} IfcMetricValueSelect",
293                        layout.version()
294                    ),
295                });
296            }
297            Value::Typed {
298                type_name: Arc::from(type_name.to_ascii_uppercase()),
299                value: Box::new(value.clone()),
300            }
301        }
302    };
303    let mut values = base;
304    values.extend([
305        (
306            "Benchmark",
307            enumerator(layout, METRIC, "Benchmark", draft.benchmark.token())?,
308        ),
309        ("ValueSource", optional_text(draft.value_source)),
310        ("DataValue", data_value),
311        ("ReferencePath", optional_ref(draft.reference_path)),
312    ]);
313    let record = layout.named_record(METRIC, values)?;
314    Ok(tx.create(record))
315}
316
317/// Validate and stage one objective in the model's declared release.
318///
319/// # Release
320///
321/// IFC4 and IFC4X3 declare `BenchmarkValues` as a `LIST OF IfcConstraint`
322/// and a `LogicalAggregator`. IFC2X3 declares a single `IfcMetric` there
323/// and `ResultValues`, an `IfcMetric`, in the aggregator's place; its
324/// `IfcObjectiveEnum` lacks several IFC4 qualifiers (`CODEWAIVER`,
325/// `EXTERNAL`, `MERGECONFLICT`, `MODELVIEW`, `PARAMETER` among them).
326///
327/// # Errors
328///
329/// A user-defined grade or qualifier without its label; an empty benchmark
330/// list or a benchmark outside its declared type; a header binding no single
331/// verified release; in IFC2X3 more than one benchmark
332/// (`AuthoringValueType`) or a logical aggregator (`AuthoringNotInSchema`);
333/// and a token the release's enumeration lacks (`AuthoringValueType`).
334/// Nothing is staged on an error.
335pub fn create_objective(
336    tx: &mut Transaction,
337    model: &Model,
338    draft: ObjectiveDraft<'_>,
339) -> ConstraintResult<EntityId> {
340    let layout = bind(model)?;
341    layout.require_entity(OBJECTIVE)?;
342    let base = base_values(layout, tx, model, OBJECTIVE, draft.base)?;
343    if draft.qualifier == ObjectiveQualifier::UserDefined && draft.user_defined_qualifier.is_none()
344    {
345        return Err(ConstraintError::AuthoringInvalid {
346            entity: OBJECTIVE,
347            attribute: "WR21",
348            value: "USERDEFINED qualifier requires UserDefinedQualifier".into(),
349        });
350    }
351    let benchmarks = match draft.benchmark_values {
352        None => Value::Null,
353        Some([]) => {
354            return Err(ConstraintError::AuthoringInvalid {
355                entity: OBJECTIVE,
356                attribute: "BenchmarkValues",
357                value: "empty LIST [1:?]".into(),
358            });
359        }
360        Some(values) => {
361            let (aggregate, expected) = layout
362                .declared(OBJECTIVE, "BenchmarkValues")
363                .map_or((true, "IfcConstraint"), |(_, declared)| {
364                    (declared.aggregate, declared.type_name.as_str())
365                });
366            for &target in values {
367                validate_target_in(layout.schema(), tx, model, target, expected)?;
368            }
369            match (aggregate, values) {
370                (true, values) => refs(values),
371                (false, [one]) => Value::Ref(*one),
372                (false, _) => {
373                    return Err(ConstraintError::AuthoringValueType {
374                        entity: OBJECTIVE,
375                        attribute: "BenchmarkValues",
376                        declared: expected,
377                        schema: layout.version(),
378                    })
379                }
380            }
381        }
382    };
383    let aggregator = match draft.logical_aggregator {
384        None => Value::Null,
385        Some(value) => enumerator(layout, OBJECTIVE, "LogicalAggregator", value.token())?,
386    };
387    let mut values = base;
388    values.extend([
389        ("BenchmarkValues", benchmarks),
390        ("LogicalAggregator", aggregator),
391        (
392            "ObjectiveQualifier",
393            enumerator(
394                layout,
395                OBJECTIVE,
396                "ObjectiveQualifier",
397                draft.qualifier.token(),
398            )?,
399        ),
400        (
401            "UserDefinedQualifier",
402            optional_text(draft.user_defined_qualifier),
403        ),
404    ]);
405    let record = layout.named_record(OBJECTIVE, values)?;
406    Ok(tx.create(record))
407}
408
409/// Validate and stage one resource-level constraint relationship in the
410/// model's declared release.
411///
412/// # Errors
413///
414/// An empty or duplicated set; a relating target that is not an
415/// `IfcConstraint` or a resource outside `IfcResourceObjectSelect`; a header
416/// binding no single verified release; and an IFC2X3 model, which declares
417/// no `IfcResourceConstraintRelationship` (`EntityNotInSchema`). Nothing is
418/// staged on an error.
419pub fn relate_resource_constraint(
420    tx: &mut Transaction,
421    model: &Model,
422    draft: ResourceConstraintDraft<'_>,
423) -> ConstraintResult<EntityId> {
424    let layout = bind(model)?;
425    layout.require_entity(RESOURCE_REL)?;
426    validate_target_in(
427        layout.schema(),
428        tx,
429        model,
430        draft.relating_constraint,
431        "IfcConstraint",
432    )?;
433    validate_set(
434        layout.schema(),
435        tx,
436        model,
437        RESOURCE_REL,
438        "RelatedResourceObjects",
439        draft.related_resources,
440        "IfcResourceObjectSelect",
441    )?;
442    let record = layout.named_record(
443        RESOURCE_REL,
444        vec![
445            ("Name", optional_text(draft.name)),
446            ("Description", optional_text(draft.description)),
447            ("RelatingConstraint", Value::Ref(draft.relating_constraint)),
448            ("RelatedResourceObjects", refs(draft.related_resources)),
449        ],
450    )?;
451    Ok(tx.create(record))
452}
453
454/// The inherited `IfcConstraint` values of `draft`, checked against the
455/// release, named for [`Layout::named_record`].
456fn base_values(
457    layout: Layout,
458    tx: &Transaction,
459    model: &Model,
460    kind: &'static str,
461    draft: ConstraintBaseDraft<'_>,
462) -> ConstraintResult<Vec<(&'static str, Value)>> {
463    if draft.grade == ConstraintGrade::UserDefined && draft.user_defined_grade.is_none() {
464        return Err(ConstraintError::AuthoringInvalid {
465            entity: kind,
466            attribute: "WR11",
467            value: "USERDEFINED grade requires UserDefinedGrade".into(),
468        });
469    }
470    if let Some(actor) = draft.creating_actor {
471        validate_target_in(layout.schema(), tx, model, actor, "IfcActorSelect")?;
472    }
473    let creation_time = match draft.creation_time {
474        None => Value::Null,
475        Some(DateTimeInput::Text(text)) => Value::Text(Arc::from(text)),
476        Some(DateTimeInput::Record(target)) => {
477            if let Some((_, declared)) = layout.declared(kind, "CreationTime") {
478                if layout.admits_entity(&declared.type_name, 8) {
479                    validate_target_in(
480                        layout.schema(),
481                        tx,
482                        model,
483                        target,
484                        declared.type_name.as_str(),
485                    )?;
486                }
487            }
488            Value::Ref(target)
489        }
490    };
491    Ok(vec![
492        ("Name", text(draft.name)),
493        ("Description", optional_text(draft.description)),
494        (
495            "ConstraintGrade",
496            enumerator(layout, kind, "ConstraintGrade", draft.grade.token())?,
497        ),
498        ("ConstraintSource", optional_text(draft.source)),
499        ("CreatingActor", optional_ref(draft.creating_actor)),
500        ("CreationTime", creation_time),
501        ("UserDefinedGrade", optional_text(draft.user_defined_grade)),
502    ])
503}
504
505/// `token` as an enumerator of the enumeration the release declares for
506/// `kind.attribute`: `AuthoringNotInSchema` when the release does not
507/// declare the attribute, `AuthoringValueType` when it lacks the token.
508fn enumerator(
509    layout: Layout,
510    kind: &'static str,
511    attribute: &'static str,
512    token: &str,
513) -> ConstraintResult<Value> {
514    let Some((_, declared)) = layout.declared(kind, attribute) else {
515        return Err(ConstraintError::AuthoringNotInSchema {
516            entity: kind,
517            attribute,
518            schema: layout.version(),
519        });
520    };
521    match layout
522        .schema()
523        .type_def(&declared.type_name)
524        .map(|t| &t.kind)
525    {
526        Some(TypeKind::Enumeration(members))
527            if members
528                .iter()
529                .any(|member| member.eq_ignore_ascii_case(token)) =>
530        {
531            Ok(enumeration(token))
532        }
533        _ => Err(ConstraintError::AuthoringValueType {
534            entity: kind,
535            attribute,
536            declared: declared.type_name.as_str(),
537            schema: layout.version(),
538        }),
539    }
540}
541
542fn validate_set(
543    schema: &Schema,
544    tx: &Transaction,
545    model: &Model,
546    kind: &'static str,
547    attribute: &'static str,
548    targets: &[EntityId],
549    expected: &'static str,
550) -> ConstraintResult<()> {
551    if targets.is_empty() {
552        return Err(ConstraintError::AuthoringInvalid {
553            entity: kind,
554            attribute,
555            value: "empty SET [1:?]".into(),
556        });
557    }
558    let mut seen = HashSet::new();
559    for &target in targets {
560        if !seen.insert(target) {
561            return Err(ConstraintError::AuthoringInvalid {
562                entity: kind,
563                attribute,
564                value: format!("duplicate {target}"),
565            });
566        }
567        validate_target_in(schema, tx, model, target, expected)?;
568    }
569    Ok(())
570}
571
572/// Draft for one `IfcReference`: a path into another entity's attributes.
573#[derive(Debug, Clone, Copy, Default)]
574#[non_exhaustive]
575pub struct ReferenceDraft<'a> {
576    /// `TypeIdentifier`, the referenced entity's type name.
577    pub type_identifier: Option<&'a str>,
578    /// `AttributeIdentifier`, the attribute being addressed.
579    pub attribute_identifier: Option<&'a str>,
580    /// `InstanceName`, naming the addressed instance.
581    pub instance_name: Option<&'a str>,
582    /// `ListPositions`, 1-based indices into list-valued attributes.
583    pub list_positions: &'a [i64],
584    /// `InnerReference`, the next step along the path.
585    pub inner_reference: Option<EntityId>,
586}
587
588impl<'a> ReferenceDraft<'a> {
589    /// Starts an empty draft; every field is unset.
590    #[must_use]
591    pub fn new() -> Self {
592        Self {
593            type_identifier: None,
594            attribute_identifier: None,
595            instance_name: None,
596            list_positions: &[],
597            inner_reference: None,
598        }
599    }
600
601    /// Sets `type_identifier`: `TypeIdentifier`, the referenced entity's type
602    /// name.
603    #[must_use]
604    pub fn type_identifier(mut self, value: &'a str) -> Self {
605        self.type_identifier = Some(value);
606        self
607    }
608
609    /// Sets `attribute_identifier`: `AttributeIdentifier`, the attribute being
610    /// addressed.
611    #[must_use]
612    pub fn attribute_identifier(mut self, value: &'a str) -> Self {
613        self.attribute_identifier = Some(value);
614        self
615    }
616
617    /// Sets `instance_name`: `InstanceName`, naming the addressed instance.
618    #[must_use]
619    pub fn instance_name(mut self, value: &'a str) -> Self {
620        self.instance_name = Some(value);
621        self
622    }
623
624    /// Sets `list_positions`: `ListPositions`, 1-based indices into list-valued
625    /// attributes.
626    #[must_use]
627    pub fn list_positions(mut self, value: &'a [i64]) -> Self {
628        self.list_positions = value;
629        self
630    }
631
632    /// Sets `inner_reference`: `InnerReference`, the next step along the path.
633    #[must_use]
634    pub fn inner_reference(mut self, value: EntityId) -> Self {
635        self.inner_reference = Some(value);
636        self
637    }
638}
639
640/// Stage an `IfcReference`.
641///
642/// A reference is a path expression: it names a type, an attribute, and
643/// optionally positions within a list, chaining through `InnerReference`
644/// to address something nested. Every slot is OPTIONAL, so the schema
645/// permits a reference that addresses nothing; that is a silently useless
646/// record, so at least one slot must be set here.
647///
648/// `ListPositions` is `LIST [1:?] OF IfcInteger` and the positions are
649/// 1-based: an empty list fails the bound, and a zero or negative index
650/// addresses no element. Both are refused rather than written.
651///
652/// # Errors
653///
654/// Refuses a fully empty draft, an empty or non-positive `ListPositions`,
655/// an `inner_reference` that is not itself an `IfcReference`, a header
656/// binding no single verified release, and an IFC2X3 model, which declares
657/// no `IfcReference` (`EntityNotInSchema`).
658pub fn create_reference(
659    tx: &mut Transaction,
660    model: &Model,
661    draft: ReferenceDraft<'_>,
662) -> ConstraintResult<EntityId> {
663    const ENTITY: &str = "IfcReference";
664    let layout = bind(model)?;
665    layout.require_entity("IFCREFERENCE")?;
666    let empty = draft.type_identifier.is_none()
667        && draft.attribute_identifier.is_none()
668        && draft.instance_name.is_none()
669        && draft.list_positions.is_empty()
670        && draft.inner_reference.is_none();
671    if empty {
672        return Err(ConstraintError::AuthoringInvalid {
673            entity: ENTITY,
674            attribute: "TypeIdentifier",
675            value: "a reference with every slot unset addresses nothing".to_owned(),
676        });
677    }
678    for position in draft.list_positions {
679        if *position < 1 {
680            return Err(ConstraintError::AuthoringInvalid {
681                entity: ENTITY,
682                attribute: "ListPositions",
683                value: format!("{position} is not a 1-based list index"),
684            });
685        }
686    }
687    if let Some(inner) = draft.inner_reference {
688        validate_target_in(layout.schema(), tx, model, inner, ENTITY)?;
689    }
690
691    let positions = if draft.list_positions.is_empty() {
692        // LIST [1:?]: absent stays null rather than becoming an
693        // empty list, which would satisfy the type and break the bound.
694        Value::Null
695    } else {
696        Value::List(
697            draft
698                .list_positions
699                .iter()
700                .copied()
701                .map(Value::Integer)
702                .collect(),
703        )
704    };
705    Ok(tx.create(Entity::new(
706        "IFCREFERENCE",
707        vec![
708            optional_text(draft.type_identifier),
709            optional_text(draft.attribute_identifier),
710            optional_text(draft.instance_name),
711            positions,
712            draft.inner_reference.map_or(Value::Null, Value::Ref),
713        ],
714    )))
715}
716
717/// Fail unless `target` resolves, in the model or staged on `tx`, to a type
718/// `schema` accepts as `expected`.
719pub(crate) fn validate_target_in(
720    schema: &Schema,
721    tx: &Transaction,
722    model: &Model,
723    target: EntityId,
724    expected: &'static str,
725) -> ConstraintResult<()> {
726    let actual =
727        final_type(tx, model, target).ok_or(ConstraintError::UnknownEntity { id: target })?;
728    if schema.accepts_type(expected, actual) {
729        Ok(())
730    } else {
731        Err(ConstraintError::AuthoringReferenceType {
732            target,
733            expected,
734            actual: actual.into(),
735        })
736    }
737}
738
739pub(crate) fn final_type<'a>(
740    tx: &'a Transaction,
741    model: &'a Model,
742    id: EntityId,
743) -> Option<&'a str> {
744    for edit in tx.edits().iter().rev() {
745        match edit {
746            Edit::Create {
747                id: edit_id,
748                entity,
749            } if *edit_id == id => return Some(&entity.type_name),
750            Edit::Remove { id: edit_id } if *edit_id == id => return None,
751            Edit::Retype {
752                id: edit_id,
753                type_name,
754            } if *edit_id == id => return Some(type_name),
755            _ => {}
756        }
757    }
758    model.get(id).map(|entity| entity.type_name.as_ref())
759}
760
761pub(crate) fn text(value: &str) -> Value {
762    Value::Text(Arc::from(value))
763}
764pub(crate) fn optional_text(value: Option<&str>) -> Value {
765    value.map_or(Value::Null, text)
766}
767pub(crate) fn optional_ref(value: Option<EntityId>) -> Value {
768    value.map_or(Value::Null, Value::Ref)
769}
770fn enumeration(value: &str) -> Value {
771    Value::Enum(Arc::from(value))
772}
773pub(crate) fn refs(values: &[EntityId]) -> Value {
774    Value::List(values.iter().copied().map(Value::Ref).collect())
775}