1use crate::bounded_deserialize::{
9 BudgetedCappedSequenceSeed, CappedSequence, RowBudget, consume_ignored_tail,
10 deserialize_capped_sequence,
11};
12use crate::{InputIdentity, SourceFormatV1};
13use serde::de::{DeserializeSeed, IgnoredAny, MapAccess, SeqAccess, Visitor};
14use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
15use std::collections::BTreeSet;
16use std::fmt;
17use std::marker::PhantomData;
18
19pub const ENGINE_PROFILE_FACTS_V1_ID: &str = "urn:animsmith:engine-profile-facts:1";
21pub const RESOLVED_ENGINE_SETTINGS_V1_ID: &str = "urn:animsmith:resolved-engine-settings:1";
23pub const ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS: usize = 4_096;
25pub const ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS: usize = 65_536;
27pub const ENGINE_CONTRACT_V1_MAX_TEXT_BYTES: usize = 4_096;
29pub const ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES: usize = 8 * 1024 * 1024;
31
32const ENGINE_FACTS_PREIMAGE_DOMAIN: &str = "animsmith-engine-facts-v1";
33const ENGINE_SETTINGS_PREIMAGE_DOMAIN: &str = "animsmith-engine-settings-v1";
34
35fn deserialize_collection_rows<'de, D, T>(deserializer: D) -> Result<CappedSequence<T>, D::Error>
36where
37 D: Deserializer<'de>,
38 T: Deserialize<'de>,
39{
40 deserialize_capped_sequence(deserializer, ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
41}
42
43#[derive(Debug)]
44struct ProfileRows {
45 local: RowBudget,
46 provenance: Option<RowBudget>,
47}
48
49impl ProfileRows {
50 fn new(provenance_limit: Option<usize>) -> Self {
51 Self {
52 local: RowBudget::new(ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS),
53 provenance: provenance_limit.map(RowBudget::new),
54 }
55 }
56
57 fn admit_top_level(&mut self) -> bool {
58 if !self.local.admit() {
59 return false;
60 }
61 self.provenance.as_mut().is_none_or(RowBudget::admit)
62 }
63
64 fn provenance_overflowed(&self) -> bool {
65 self.provenance.as_ref().is_some_and(RowBudget::overflowed)
66 }
67}
68
69#[derive(Debug)]
70struct SettingsRows {
71 local: RowBudget,
72 provenance: Option<RowBudget>,
73}
74
75impl SettingsRows {
76 fn new(provenance_limit: Option<usize>) -> Self {
77 Self {
78 local: RowBudget::new(ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS),
79 provenance: provenance_limit.map(RowBudget::new),
80 }
81 }
82
83 fn admit_clip(&mut self) -> bool {
84 self.local.admit()
85 }
86
87 fn admit_setting(&mut self) -> bool {
88 if !self.local.admit() {
89 return false;
90 }
91 self.provenance.as_mut().is_none_or(RowBudget::admit)
92 }
93
94 fn provenance_overflowed(&self) -> bool {
95 self.provenance.as_ref().is_some_and(RowBudget::overflowed)
96 }
97}
98
99#[derive(Debug, Default)]
104pub(crate) struct CanonicalEncoder(Vec<u8>);
105
106impl CanonicalEncoder {
107 pub(crate) fn new(domain: &str) -> Self {
109 let mut encoder = Self::default();
110 encoder.token(domain);
111 encoder
112 }
113
114 pub(crate) fn token(&mut self, token: impl AsRef<str>) {
116 let bytes = token.as_ref().as_bytes();
117 self.0
118 .extend_from_slice(&(bytes.len() as u64).to_be_bytes());
119 self.0.extend_from_slice(bytes);
120 }
121
122 pub(crate) fn field(&mut self, field: &'static str) {
124 self.token(field);
125 }
126
127 pub(crate) fn count(&mut self, count: usize) {
129 self.token(count.to_string());
130 }
131
132 pub(crate) fn identity(self) -> InputIdentity {
134 InputIdentity::from_bytes(&self.0)
135 }
136
137 pub(crate) fn into_bytes(self) -> Vec<u8> {
139 self.0
140 }
141}
142
143pub(crate) fn encode_input_identity(encoder: &mut CanonicalEncoder, identity: &InputIdentity) {
145 encoder.token("sha256");
146 encoder.token(identity.sha256());
147 encoder.token("bytes");
148 encoder.token(identity.bytes().to_string());
149}
150
151impl Serialize for SourceFormatV1 {
152 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
153 where
154 S: Serializer,
155 {
156 serializer.serialize_str(source_format_name(*self))
157 }
158}
159
160impl<'de> Deserialize<'de> for SourceFormatV1 {
161 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162 where
163 D: Deserializer<'de>,
164 {
165 match String::deserialize(deserializer)?.as_str() {
166 "gltf_json" => Ok(Self::GltfJson),
167 "glb" => Ok(Self::Glb),
168 "fbx" => Ok(Self::Fbx),
169 other => Err(D::Error::custom(format!(
170 "unknown V1 source format {other:?}"
171 ))),
172 }
173 }
174}
175
176impl<'de> Deserialize<'de> for InputIdentity {
177 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
178 where
179 D: Deserializer<'de>,
180 {
181 #[derive(Deserialize)]
182 #[serde(deny_unknown_fields)]
183 struct WireIdentity {
184 sha256: String,
185 bytes: u64,
186 }
187
188 let wire = WireIdentity::deserialize(deserializer)?;
189 if wire.sha256.len() != 64
190 || !wire
191 .sha256
192 .bytes()
193 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
194 {
195 return Err(D::Error::custom(
196 "input identity sha256 must be exactly 64 lowercase hexadecimal digits",
197 ));
198 }
199 let mut digest = [0_u8; 32];
200 for (index, pair) in wire.sha256.as_bytes().as_chunks::<2>().0.iter().enumerate() {
201 digest[index] = (hex_nibble(pair[0]).expect("validated hexadecimal") << 4)
202 | hex_nibble(pair[1]).expect("validated hexadecimal");
203 }
204 Ok(InputIdentity::from_sha256_digest(digest, wire.bytes))
205 }
206}
207
208fn hex_nibble(byte: u8) -> Option<u8> {
209 match byte {
210 b'0'..=b'9' => Some(byte - b'0'),
211 b'a'..=b'f' => Some(byte - b'a' + 10),
212 _ => None,
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
218#[serde(deny_unknown_fields)]
219pub struct EngineProfileSelectionV1 {
220 family: String,
221 profile_revision: u32,
222 engine_version: String,
223 importer: String,
224}
225
226impl EngineProfileSelectionV1 {
227 pub fn new(
234 family: impl Into<String>,
235 profile_revision: u32,
236 engine_version: impl Into<String>,
237 importer: impl Into<String>,
238 ) -> Result<Self, EngineContractError> {
239 let selection = Self {
240 family: family.into(),
241 profile_revision,
242 engine_version: engine_version.into(),
243 importer: importer.into(),
244 };
245 selection.validate()?;
246 Ok(selection)
247 }
248
249 pub fn family(&self) -> &str {
251 &self.family
252 }
253
254 pub const fn profile_revision(&self) -> u32 {
256 self.profile_revision
257 }
258
259 pub fn engine_version(&self) -> &str {
261 &self.engine_version
262 }
263
264 pub fn importer(&self) -> &str {
266 &self.importer
267 }
268
269 fn validate(&self) -> Result<(), EngineContractError> {
270 validate_required_text("selection.family", &self.family)?;
271 validate_required_text("selection.engine_version", &self.engine_version)?;
272 validate_required_text("selection.importer", &self.importer)
273 }
274
275 fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
276 checked_sum(
277 "profile retained text",
278 [
279 self.family.len(),
280 self.engine_version.len(),
281 self.importer.len(),
282 ],
283 )
284 }
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum EngineFactIdV1 {
291 AcceptedInputs,
293 AnimationAddressability,
295 TargetCoordinateBasis,
297 TargetLinearUnit,
299 UnitConversionControl,
301 AxisConversionControl,
303 ExactAxisConversion,
305 ResultingHierarchyScale,
307 WholeEndFrameRequired,
309 AnimationChannelHandling,
311 ExtensionHandling,
313 ConstructHandling,
315 AnimationTargetAddressability,
317 RootMotionAddressability,
319}
320
321impl EngineFactIdV1 {
322 pub const fn as_str(self) -> &'static str {
324 match self {
325 Self::AcceptedInputs => "accepted_inputs",
326 Self::AnimationAddressability => "animation_addressability",
327 Self::TargetCoordinateBasis => "target_coordinate_basis",
328 Self::TargetLinearUnit => "target_linear_unit",
329 Self::UnitConversionControl => "unit_conversion_control",
330 Self::AxisConversionControl => "axis_conversion_control",
331 Self::ExactAxisConversion => "exact_axis_conversion",
332 Self::ResultingHierarchyScale => "resulting_hierarchy_scale",
333 Self::WholeEndFrameRequired => "whole_end_frame_required",
334 Self::AnimationChannelHandling => "animation_channel_handling",
335 Self::ExtensionHandling => "extension_handling",
336 Self::ConstructHandling => "construct_handling",
337 Self::AnimationTargetAddressability => "animation_target_addressability",
338 Self::RootMotionAddressability => "root_motion_addressability",
339 }
340 }
341}
342
343const ALL_FACT_IDS: [EngineFactIdV1; 14] = [
344 EngineFactIdV1::AcceptedInputs,
345 EngineFactIdV1::AnimationAddressability,
346 EngineFactIdV1::AnimationChannelHandling,
347 EngineFactIdV1::AnimationTargetAddressability,
348 EngineFactIdV1::AxisConversionControl,
349 EngineFactIdV1::ConstructHandling,
350 EngineFactIdV1::ExactAxisConversion,
351 EngineFactIdV1::ExtensionHandling,
352 EngineFactIdV1::ResultingHierarchyScale,
353 EngineFactIdV1::RootMotionAddressability,
354 EngineFactIdV1::TargetCoordinateBasis,
355 EngineFactIdV1::TargetLinearUnit,
356 EngineFactIdV1::UnitConversionControl,
357 EngineFactIdV1::WholeEndFrameRequired,
358];
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
362#[serde(rename_all = "snake_case")]
363pub enum EngineHandednessV1 {
364 Left,
366 Right,
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
372#[serde(rename_all = "snake_case")]
373pub enum EngineUpAxisV1 {
374 X,
376 Y,
378 Z,
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
384#[serde(rename_all = "snake_case")]
385pub enum EngineForwardAxisV1 {
386 PositiveX,
388 NegativeX,
390 PositiveY,
392 NegativeY,
394 PositiveZ,
396 NegativeZ,
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
402#[serde(deny_unknown_fields)]
403pub struct EngineCoordinateBasisV1 {
404 pub handedness: EngineHandednessV1,
406 pub up_axis: EngineUpAxisV1,
408 pub forward_axis: EngineForwardAxisV1,
410}
411
412#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
414#[serde(rename_all = "snake_case")]
415pub enum EngineLinearUnitV1 {
416 Metre,
418 Centimetre,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
424#[serde(rename_all = "snake_case")]
425pub enum EngineSettingIdV1 {
426 ConvertUnits,
428 BakeAxisConversion,
430 RootMotionSource,
432 RootRotation,
434 RootPositionY,
436 RootPositionXz,
438}
439
440impl EngineSettingIdV1 {
441 pub const fn as_str(self) -> &'static str {
443 match self {
444 Self::ConvertUnits => "convert_units",
445 Self::BakeAxisConversion => "bake_axis_conversion",
446 Self::RootMotionSource => "root_motion_source",
447 Self::RootRotation => "root_rotation",
448 Self::RootPositionY => "root_position_y",
449 Self::RootPositionXz => "root_position_xz",
450 }
451 }
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
456#[serde(rename_all = "snake_case")]
457pub enum EngineConversionControlV1 {
458 ProfileSetting(EngineSettingIdV1),
460 ImporterOption,
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
466#[serde(rename_all = "snake_case")]
467pub enum EngineImportHandlingV1 {
468 Preserved,
470 Converted,
472 Discarded,
474 Unsupported,
476}
477
478#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
480#[serde(rename_all = "snake_case")]
481pub enum EngineTargetAddressabilityV1 {
482 NamePathDerivedId,
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
488#[serde(rename_all = "snake_case")]
489pub enum EngineAnimationAddressabilityV1 {
490 GltfAssetLabel,
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
498#[serde(rename_all = "snake_case")]
499pub enum EngineRootMotionAddressabilityV1 {
500 ExactSourceTransformPath,
502 HumanoidAvatarBody,
504}
505
506#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
508#[serde(rename_all = "snake_case")]
509pub enum EngineFactValueV1 {
510 AcceptedFormats(Vec<SourceFormatV1>),
512 AnimationAddressability(EngineAnimationAddressabilityV1),
514 CoordinateBasis(EngineCoordinateBasisV1),
516 LinearUnit(EngineLinearUnitV1),
518 ConversionControl(EngineConversionControlV1),
520 Boolean(bool),
522 ImportHandling(EngineImportHandlingV1),
524 TargetAddressability(EngineTargetAddressabilityV1),
526 RootMotionAddressability(EngineRootMotionAddressabilityV1),
528}
529
530#[derive(Deserialize)]
531#[serde(rename_all = "snake_case")]
532enum EngineFactValueWireV1 {
533 AcceptedFormats(
534 #[serde(deserialize_with = "deserialize_collection_rows")] CappedSequence<SourceFormatV1>,
535 ),
536 AnimationAddressability(EngineAnimationAddressabilityV1),
537 CoordinateBasis(EngineCoordinateBasisV1),
538 LinearUnit(EngineLinearUnitV1),
539 ConversionControl(EngineConversionControlV1),
540 Boolean(bool),
541 ImportHandling(EngineImportHandlingV1),
542 TargetAddressability(EngineTargetAddressabilityV1),
543 RootMotionAddressability(EngineRootMotionAddressabilityV1),
544}
545
546impl TryFrom<EngineFactValueWireV1> for EngineFactValueV1 {
547 type Error = EngineContractError;
548
549 fn try_from(wire: EngineFactValueWireV1) -> Result<Self, Self::Error> {
550 Ok(match wire {
551 EngineFactValueWireV1::AcceptedFormats(formats) => {
552 if formats.overflowed {
553 return Err(EngineContractError::InvalidAcceptedInputs);
554 }
555 Self::AcceptedFormats(formats.values)
556 }
557 EngineFactValueWireV1::AnimationAddressability(value) => {
558 Self::AnimationAddressability(value)
559 }
560 EngineFactValueWireV1::CoordinateBasis(value) => Self::CoordinateBasis(value),
561 EngineFactValueWireV1::LinearUnit(value) => Self::LinearUnit(value),
562 EngineFactValueWireV1::ConversionControl(value) => Self::ConversionControl(value),
563 EngineFactValueWireV1::Boolean(value) => Self::Boolean(value),
564 EngineFactValueWireV1::ImportHandling(value) => Self::ImportHandling(value),
565 EngineFactValueWireV1::TargetAddressability(value) => Self::TargetAddressability(value),
566 EngineFactValueWireV1::RootMotionAddressability(value) => {
567 Self::RootMotionAddressability(value)
568 }
569 })
570 }
571}
572
573impl<'de> Deserialize<'de> for EngineFactValueV1 {
574 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
575 where
576 D: Deserializer<'de>,
577 {
578 EngineFactValueWireV1::deserialize(deserializer)?
579 .try_into()
580 .map_err(D::Error::custom)
581 }
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
586#[serde(rename_all = "snake_case")]
587pub enum EngineFactStateV1 {
588 Known(EngineFactValueV1),
590 Unknown,
592 NotApplicable,
594}
595
596#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
598#[serde(deny_unknown_fields)]
599pub struct EngineProfileFactV1 {
600 id: EngineFactIdV1,
601 state: EngineFactStateV1,
602}
603
604impl EngineProfileFactV1 {
605 pub const fn new(id: EngineFactIdV1, state: EngineFactStateV1) -> Self {
607 Self { id, state }
608 }
609
610 pub const fn id(&self) -> EngineFactIdV1 {
612 self.id
613 }
614
615 pub const fn state(&self) -> &EngineFactStateV1 {
617 &self.state
618 }
619}
620
621#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
623#[serde(rename_all = "snake_case")]
624pub enum EngineSettingScopeV1 {
625 Document,
627 Clip,
629}
630
631#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
633#[serde(rename_all = "snake_case")]
634pub enum EngineSettingDomainV1 {
635 Boolean,
637 BakeOrExtract,
639 SourceTransformPath,
641}
642
643#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
645#[serde(rename_all = "snake_case")]
646pub enum EngineSettingApplicabilityV1 {
647 Applicable,
649 NotApplicable,
651}
652
653#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
655#[serde(rename_all = "snake_case")]
656pub enum EngineDefaultStatusV1 {
657 RequiredWithoutDefault,
659 NotApplicable,
661}
662
663#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
665#[serde(deny_unknown_fields)]
666pub struct EngineSettingDescriptorV1 {
667 id: EngineSettingIdV1,
668 scope: EngineSettingScopeV1,
669 domain: EngineSettingDomainV1,
670 applicability: EngineSettingApplicabilityV1,
671 default_status: EngineDefaultStatusV1,
672}
673
674impl EngineSettingDescriptorV1 {
675 pub const fn new(
677 id: EngineSettingIdV1,
678 scope: EngineSettingScopeV1,
679 domain: EngineSettingDomainV1,
680 applicability: EngineSettingApplicabilityV1,
681 default_status: EngineDefaultStatusV1,
682 ) -> Self {
683 Self {
684 id,
685 scope,
686 domain,
687 applicability,
688 default_status,
689 }
690 }
691
692 pub const fn id(&self) -> EngineSettingIdV1 {
694 self.id
695 }
696
697 pub const fn scope(&self) -> EngineSettingScopeV1 {
699 self.scope
700 }
701
702 pub const fn domain(&self) -> EngineSettingDomainV1 {
704 self.domain
705 }
706
707 pub const fn applicability(&self) -> EngineSettingApplicabilityV1 {
709 self.applicability
710 }
711
712 pub const fn default_status(&self) -> EngineDefaultStatusV1 {
714 self.default_status
715 }
716}
717
718#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
720#[serde(deny_unknown_fields)]
721pub struct EnginePrimarySourceV1 {
722 id: String,
723 target_version: String,
724 url: String,
725 verified_on: String,
726 supported_fact_ids: Vec<EngineFactIdV1>,
727 supported_setting_ids: Vec<EngineSettingIdV1>,
728}
729
730#[derive(Deserialize)]
731#[serde(deny_unknown_fields)]
732struct EnginePrimarySourceWireV1 {
733 id: String,
734 target_version: String,
735 url: String,
736 verified_on: String,
737 #[serde(deserialize_with = "deserialize_collection_rows")]
738 supported_fact_ids: CappedSequence<EngineFactIdV1>,
739 #[serde(deserialize_with = "deserialize_collection_rows")]
740 supported_setting_ids: CappedSequence<EngineSettingIdV1>,
741}
742
743struct EnginePrimarySourceSeed<'a> {
744 rows: &'a mut ProfileRows,
745}
746
747impl<'de> DeserializeSeed<'de> for EnginePrimarySourceSeed<'_> {
748 type Value = EnginePrimarySourceWireV1;
749
750 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
751 where
752 D: Deserializer<'de>,
753 {
754 #[derive(Deserialize)]
755 #[serde(field_identifier, rename_all = "snake_case")]
756 enum Field {
757 Id,
758 TargetVersion,
759 Url,
760 VerifiedOn,
761 SupportedFactIds,
762 SupportedSettingIds,
763 }
764
765 struct PrimarySourceVisitor<'a> {
766 rows: &'a mut ProfileRows,
767 }
768
769 impl<'de> Visitor<'de> for PrimarySourceVisitor<'_> {
770 type Value = EnginePrimarySourceWireV1;
771
772 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
773 formatter.write_str("an engine primary-source record")
774 }
775
776 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
777 where
778 A: MapAccess<'de>,
779 {
780 let mut id = None;
781 let mut target_version = None;
782 let mut url = None;
783 let mut verified_on = None;
784 let mut supported_fact_ids = None;
785 let mut supported_setting_ids = None;
786 while let Some(field) = map.next_key()? {
787 match field {
788 Field::Id => set_once(&mut id, map.next_value()?, "id")?,
789 Field::TargetVersion => {
790 set_once(&mut target_version, map.next_value()?, "target_version")?
791 }
792 Field::Url => set_once(&mut url, map.next_value()?, "url")?,
793 Field::VerifiedOn => {
794 set_once(&mut verified_on, map.next_value()?, "verified_on")?
795 }
796 Field::SupportedFactIds => {
797 if supported_fact_ids.is_some() {
798 return Err(A::Error::duplicate_field("supported_fact_ids"));
799 }
800 supported_fact_ids =
801 Some(map.next_value_seed(BudgetedCappedSequenceSeed {
802 budget: &mut self.rows.local,
803 local_limit: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
804 element: PhantomData,
805 })?);
806 }
807 Field::SupportedSettingIds => {
808 if supported_setting_ids.is_some() {
809 return Err(A::Error::duplicate_field("supported_setting_ids"));
810 }
811 supported_setting_ids =
812 Some(map.next_value_seed(BudgetedCappedSequenceSeed {
813 budget: &mut self.rows.local,
814 local_limit: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
815 element: PhantomData,
816 })?);
817 }
818 }
819 }
820 Ok(EnginePrimarySourceWireV1 {
821 id: required(id, "id")?,
822 target_version: required(target_version, "target_version")?,
823 url: required(url, "url")?,
824 verified_on: required(verified_on, "verified_on")?,
825 supported_fact_ids: required(supported_fact_ids, "supported_fact_ids")?,
826 supported_setting_ids: required(
827 supported_setting_ids,
828 "supported_setting_ids",
829 )?,
830 })
831 }
832 }
833
834 deserializer.deserialize_struct(
835 "EnginePrimarySourceV1",
836 &[
837 "id",
838 "target_version",
839 "url",
840 "verified_on",
841 "supported_fact_ids",
842 "supported_setting_ids",
843 ],
844 PrimarySourceVisitor { rows: self.rows },
845 )
846 }
847}
848
849fn set_once<E, T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), E>
850where
851 E: serde::de::Error,
852{
853 if slot.replace(value).is_some() {
854 return Err(E::duplicate_field(field));
855 }
856 Ok(())
857}
858
859fn required<E, T>(value: Option<T>, field: &'static str) -> Result<T, E>
860where
861 E: serde::de::Error,
862{
863 value.ok_or_else(|| E::missing_field(field))
864}
865
866impl EnginePrimarySourceV1 {
867 fn from_wire(wire: EnginePrimarySourceWireV1) -> Result<Self, EngineContractError> {
868 validate_required_text("primary_sources.id", &wire.id)?;
869 validate_required_text("primary_sources.target_version", &wire.target_version)?;
870 validate_required_text("primary_sources.url", &wire.url)?;
871 validate_required_text("primary_sources.verified_on", &wire.verified_on)?;
872 if wire.supported_fact_ids.overflowed {
873 return Err(EngineContractError::TooManyRows {
874 field: "primary_sources.supported_fact_ids",
875 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
876 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
877 });
878 }
879 if wire.supported_setting_ids.overflowed {
880 return Err(EngineContractError::TooManyRows {
881 field: "primary_sources.supported_setting_ids",
882 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
883 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
884 });
885 }
886 let source = Self {
887 id: wire.id,
888 target_version: wire.target_version,
889 url: wire.url,
890 verified_on: wire.verified_on,
891 supported_fact_ids: wire.supported_fact_ids.values,
892 supported_setting_ids: wire.supported_setting_ids.values,
893 };
894 source.validate(true)?;
895 Ok(source)
896 }
897}
898
899impl<'de> Deserialize<'de> for EnginePrimarySourceV1 {
900 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
901 where
902 D: Deserializer<'de>,
903 {
904 Self::from_wire(EnginePrimarySourceWireV1::deserialize(deserializer)?)
905 .map_err(D::Error::custom)
906 }
907}
908
909impl EnginePrimarySourceV1 {
910 pub fn new(
917 id: impl Into<String>,
918 target_version: impl Into<String>,
919 url: impl Into<String>,
920 verified_on: impl Into<String>,
921 mut supported_fact_ids: Vec<EngineFactIdV1>,
922 mut supported_setting_ids: Vec<EngineSettingIdV1>,
923 ) -> Result<Self, EngineContractError> {
924 supported_fact_ids.sort_by_key(|id| id.as_str());
925 supported_setting_ids.sort_by_key(|id| id.as_str());
926 let source = Self {
927 id: id.into(),
928 target_version: target_version.into(),
929 url: url.into(),
930 verified_on: verified_on.into(),
931 supported_fact_ids,
932 supported_setting_ids,
933 };
934 source.validate(true)?;
935 Ok(source)
936 }
937
938 pub fn id(&self) -> &str {
940 &self.id
941 }
942
943 pub fn target_version(&self) -> &str {
945 &self.target_version
946 }
947
948 pub fn url(&self) -> &str {
950 &self.url
951 }
952
953 pub fn verified_on(&self) -> &str {
955 &self.verified_on
956 }
957
958 pub fn supported_fact_ids(&self) -> &[EngineFactIdV1] {
960 &self.supported_fact_ids
961 }
962
963 pub fn supported_setting_ids(&self) -> &[EngineSettingIdV1] {
965 &self.supported_setting_ids
966 }
967
968 fn validate(&self, require_order: bool) -> Result<(), EngineContractError> {
969 validate_required_text("primary_sources.id", &self.id)?;
970 validate_required_text("primary_sources.target_version", &self.target_version)?;
971 validate_required_text("primary_sources.url", &self.url)?;
972 validate_required_text("primary_sources.verified_on", &self.verified_on)?;
973 validate_collection_len(
974 "primary_sources.supported_fact_ids",
975 self.supported_fact_ids.len(),
976 )?;
977 validate_collection_len(
978 "primary_sources.supported_setting_ids",
979 self.supported_setting_ids.len(),
980 )?;
981 validate_unique_order(
982 "primary_sources.supported_fact_ids",
983 &self.supported_fact_ids,
984 |id| id.as_str(),
985 require_order,
986 )?;
987 validate_unique_order(
988 "primary_sources.supported_setting_ids",
989 &self.supported_setting_ids,
990 |id| id.as_str(),
991 require_order,
992 )
993 }
994
995 fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
996 checked_sum(
997 "profile retained text",
998 [
999 self.id.len(),
1000 self.target_version.len(),
1001 self.url.len(),
1002 self.verified_on.len(),
1003 ],
1004 )
1005 }
1006}
1007
1008#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1010pub struct ResolvedEngineProfileV1 {
1011 schema: String,
1012 selection: EngineProfileSelectionV1,
1013 fact_bundle_urn: String,
1014 identity: InputIdentity,
1015 facts: Vec<EngineProfileFactV1>,
1016 setting_descriptors: Vec<EngineSettingDescriptorV1>,
1017 primary_sources: Vec<EnginePrimarySourceV1>,
1018}
1019
1020impl ResolvedEngineProfileV1 {
1021 pub fn new(
1028 selection: EngineProfileSelectionV1,
1029 fact_bundle_urn: impl Into<String>,
1030 mut facts: Vec<EngineProfileFactV1>,
1031 mut setting_descriptors: Vec<EngineSettingDescriptorV1>,
1032 mut primary_sources: Vec<EnginePrimarySourceV1>,
1033 ) -> Result<Self, EngineContractError> {
1034 for fact in &mut facts {
1035 if let EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(formats)) =
1036 &mut fact.state
1037 {
1038 formats.sort_by_key(|format| source_format_name(*format));
1039 }
1040 }
1041 facts.sort_by_key(|fact| fact.id.as_str());
1042 setting_descriptors.sort_by_key(|descriptor| descriptor.id.as_str());
1043 primary_sources.sort_by(|left, right| left.id.cmp(&right.id));
1044 let mut profile = Self {
1045 schema: ENGINE_PROFILE_FACTS_V1_ID.to_owned(),
1046 selection,
1047 fact_bundle_urn: fact_bundle_urn.into(),
1048 identity: InputIdentity::from_bytes(&[]),
1049 facts,
1050 setting_descriptors,
1051 primary_sources,
1052 };
1053 profile.validate_semantics(true, false)?;
1054 profile.identity = profile.computed_identity();
1055 Ok(profile)
1056 }
1057
1058 pub fn contract_id(&self) -> &str {
1060 &self.schema
1061 }
1062
1063 pub const fn selection(&self) -> &EngineProfileSelectionV1 {
1065 &self.selection
1066 }
1067
1068 pub fn fact_bundle_urn(&self) -> &str {
1070 &self.fact_bundle_urn
1071 }
1072
1073 pub const fn facts_identity(&self) -> &InputIdentity {
1075 &self.identity
1076 }
1077
1078 pub fn facts(&self) -> &[EngineProfileFactV1] {
1080 &self.facts
1081 }
1082
1083 pub fn setting_descriptors(&self) -> &[EngineSettingDescriptorV1] {
1085 &self.setting_descriptors
1086 }
1087
1088 pub fn primary_sources(&self) -> &[EnginePrimarySourceV1] {
1090 &self.primary_sources
1091 }
1092
1093 pub fn fact(&self, id: EngineFactIdV1) -> Option<&EngineProfileFactV1> {
1095 self.facts.iter().find(|fact| fact.id == id)
1096 }
1097
1098 pub fn accepts_format(&self, format: SourceFormatV1) -> bool {
1100 matches!(
1101 self.fact(EngineFactIdV1::AcceptedInputs)
1102 .map(EngineProfileFactV1::state),
1103 Some(EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(formats)))
1104 if formats.contains(&format)
1105 )
1106 }
1107
1108 pub fn setting_descriptor(&self, id: EngineSettingIdV1) -> Option<&EngineSettingDescriptorV1> {
1110 self.setting_descriptors
1111 .iter()
1112 .find(|descriptor| descriptor.id == id)
1113 }
1114
1115 pub fn source(&self, id: &str) -> Option<&EnginePrimarySourceV1> {
1117 self.primary_sources.iter().find(|source| source.id == id)
1118 }
1119
1120 pub fn validate(&self) -> Result<(), EngineContractError> {
1126 self.validate_semantics(true, true)
1127 }
1128
1129 pub(crate) fn encode_preimage(&self, encoder: &mut CanonicalEncoder) {
1131 encoder.token(ENGINE_FACTS_PREIMAGE_DOMAIN);
1132 encode_profile_key(encoder, &self.selection);
1133 encoder.field("fact_bundle_urn");
1134 encoder.token(&self.fact_bundle_urn);
1135 encoder.field("facts");
1136 encoder.count(self.facts.len());
1137 for fact in &self.facts {
1138 encoder.token(fact.id.as_str());
1139 encode_fact_state(encoder, &fact.state);
1140 }
1141 encoder.field("setting_descriptors");
1142 encoder.count(self.setting_descriptors.len());
1143 for descriptor in &self.setting_descriptors {
1144 encoder.token(descriptor.id.as_str());
1145 encoder.token(setting_scope_name(descriptor.scope));
1146 encoder.token(setting_domain_name(descriptor.domain));
1147 encoder.token(match descriptor.applicability {
1148 EngineSettingApplicabilityV1::Applicable => "applicable",
1149 EngineSettingApplicabilityV1::NotApplicable => "not_applicable",
1150 });
1151 encoder.token(match descriptor.default_status {
1152 EngineDefaultStatusV1::RequiredWithoutDefault => "required_without_default",
1153 EngineDefaultStatusV1::NotApplicable => "not_applicable",
1154 });
1155 }
1156 encoder.field("sources");
1157 encoder.count(self.primary_sources.len());
1158 for source in &self.primary_sources {
1159 encoder.token(&source.id);
1160 encoder.token(&source.target_version);
1161 encoder.token(&source.url);
1162 encoder.token(&source.verified_on);
1163 encoder.count(source.supported_fact_ids.len());
1164 for id in &source.supported_fact_ids {
1165 encoder.token(id.as_str());
1166 }
1167 encoder.count(source.supported_setting_ids.len());
1168 for id in &source.supported_setting_ids {
1169 encoder.token(id.as_str());
1170 }
1171 }
1172 }
1173
1174 pub(crate) fn retained_rows(&self) -> Result<usize, EngineContractError> {
1175 let nested = self.primary_sources.iter().map(|source| {
1176 source
1177 .supported_fact_ids
1178 .len()
1179 .checked_add(source.supported_setting_ids.len())
1180 .ok_or(EngineContractError::ArithmeticOverflow {
1181 field: "profile retained rows",
1182 })
1183 });
1184 checked_sum_results(
1185 "profile retained rows",
1186 [
1187 self.facts.len(),
1188 self.setting_descriptors.len(),
1189 self.primary_sources.len(),
1190 ],
1191 nested,
1192 )
1193 }
1194
1195 pub(crate) fn provenance_rows(&self) -> usize {
1196 self.facts
1197 .len()
1198 .saturating_add(self.setting_descriptors.len())
1199 .saturating_add(self.primary_sources.len())
1200 }
1201
1202 pub(crate) fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
1203 let base = self
1204 .selection
1205 .retained_text_bytes()?
1206 .checked_add(self.fact_bundle_urn.len())
1207 .ok_or(EngineContractError::ArithmeticOverflow {
1208 field: "profile retained text",
1209 })?;
1210 checked_sum_results(
1211 "profile retained text",
1212 [base],
1213 self.primary_sources
1214 .iter()
1215 .map(EnginePrimarySourceV1::retained_text_bytes),
1216 )
1217 }
1218
1219 fn computed_identity(&self) -> InputIdentity {
1220 let mut encoder = CanonicalEncoder::default();
1221 self.encode_preimage(&mut encoder);
1222 encoder.identity()
1223 }
1224
1225 fn validate_semantics(
1226 &self,
1227 require_order: bool,
1228 verify_identity: bool,
1229 ) -> Result<(), EngineContractError> {
1230 validate_schema("profile.schema", &self.schema, ENGINE_PROFILE_FACTS_V1_ID)?;
1231 self.selection.validate()?;
1232 validate_required_text("profile.fact_bundle_urn", &self.fact_bundle_urn)?;
1233 validate_collection_len("profile.facts", self.facts.len())?;
1234 validate_collection_len(
1235 "profile.setting_descriptors",
1236 self.setting_descriptors.len(),
1237 )?;
1238 validate_collection_len("profile.primary_sources", self.primary_sources.len())?;
1239 validate_unique_order(
1240 "profile.facts",
1241 &self.facts,
1242 |fact| fact.id.as_str(),
1243 require_order,
1244 )?;
1245 if self.facts.len() != ALL_FACT_IDS.len()
1246 || !self
1247 .facts
1248 .iter()
1249 .zip(ALL_FACT_IDS)
1250 .all(|(fact, expected)| fact.id == expected)
1251 {
1252 return Err(EngineContractError::InvalidFactInventory);
1253 }
1254 for fact in &self.facts {
1255 validate_fact_value(fact)?;
1256 if let EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
1257 EngineConversionControlV1::ProfileSetting(setting),
1258 )) = &fact.state
1259 && self.setting_descriptor(*setting).is_none()
1260 {
1261 return Err(EngineContractError::InvalidFactValue { fact: fact.id });
1262 }
1263 }
1264 if !matches!(
1265 self.fact(EngineFactIdV1::AcceptedInputs)
1266 .map(EngineProfileFactV1::state),
1267 Some(EngineFactStateV1::Known(
1268 EngineFactValueV1::AcceptedFormats(formats)
1269 )) if !formats.is_empty()
1270 ) {
1271 return Err(EngineContractError::InvalidAcceptedInputs);
1272 }
1273 validate_unique_order(
1274 "profile.setting_descriptors",
1275 &self.setting_descriptors,
1276 |descriptor| descriptor.id.as_str(),
1277 require_order,
1278 )?;
1279 for descriptor in &self.setting_descriptors {
1280 if !matches!(
1281 (descriptor.applicability, descriptor.default_status),
1282 (
1283 EngineSettingApplicabilityV1::Applicable,
1284 EngineDefaultStatusV1::RequiredWithoutDefault
1285 ) | (
1286 EngineSettingApplicabilityV1::NotApplicable,
1287 EngineDefaultStatusV1::NotApplicable
1288 )
1289 ) {
1290 return Err(EngineContractError::InvalidDescriptorDefault {
1291 setting: descriptor.id,
1292 });
1293 }
1294 }
1295 validate_unique_order(
1296 "profile.primary_sources",
1297 &self.primary_sources,
1298 |source| source.id.as_str(),
1299 require_order,
1300 )?;
1301 for source in &self.primary_sources {
1302 source.validate(require_order)?;
1303 for fact in &source.supported_fact_ids {
1304 let Some(row) = self.fact(*fact) else {
1305 return Err(EngineContractError::UnknownSourceFact {
1306 source_id: source.id.clone(),
1307 fact: *fact,
1308 });
1309 };
1310 if !matches!(row.state, EngineFactStateV1::Known(_)) {
1311 return Err(EngineContractError::SourceReferencesNonKnownFact {
1312 source_id: source.id.clone(),
1313 fact: *fact,
1314 });
1315 }
1316 }
1317 for setting in &source.supported_setting_ids {
1318 if self.setting_descriptor(*setting).is_none() {
1319 return Err(EngineContractError::UnknownSourceSetting {
1320 source_id: source.id.clone(),
1321 setting: *setting,
1322 });
1323 }
1324 }
1325 }
1326 for fact in &self.facts {
1327 if matches!(fact.state, EngineFactStateV1::Known(_))
1328 && !self
1329 .primary_sources
1330 .iter()
1331 .any(|source| source.supported_fact_ids.contains(&fact.id))
1332 {
1333 return Err(EngineContractError::UnreferencedKnownFact { fact: fact.id });
1334 }
1335 }
1336 for descriptor in &self.setting_descriptors {
1337 if !self
1338 .primary_sources
1339 .iter()
1340 .any(|source| source.supported_setting_ids.contains(&descriptor.id))
1341 {
1342 return Err(EngineContractError::UnreferencedSetting {
1343 setting: descriptor.id,
1344 });
1345 }
1346 }
1347 let rows = self.retained_rows()?;
1348 if rows > ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS {
1349 return Err(EngineContractError::TooManyAggregateRows {
1350 found: rows,
1351 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
1352 });
1353 }
1354 let text = self.retained_text_bytes()?;
1355 if text > ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES {
1356 return Err(EngineContractError::TooMuchAggregateText {
1357 found: text,
1358 max: ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES,
1359 });
1360 }
1361 if verify_identity && self.identity != self.computed_identity() {
1362 return Err(EngineContractError::IdentityMismatch {
1363 contract: ENGINE_PROFILE_FACTS_V1_ID,
1364 });
1365 }
1366 Ok(())
1367 }
1368}
1369
1370struct ResolvedEngineProfileWireV1 {
1371 schema: String,
1372 selection: EngineProfileSelectionV1,
1373 fact_bundle_urn: String,
1374 identity: InputIdentity,
1375 facts: CappedSequence<EngineProfileFactV1>,
1376 setting_descriptors: CappedSequence<EngineSettingDescriptorV1>,
1377 primary_sources: CappedSequence<EnginePrimarySourceWireV1>,
1378 aggregate_rows: RowBudget,
1379 provenance_rows_overflowed: bool,
1380}
1381
1382enum ProfileTopLevelElement<T> {
1383 Value(T),
1384 Skipped,
1385}
1386
1387struct ProfileTopLevelElementSeed<'a, T> {
1388 rows: &'a mut ProfileRows,
1389 element: PhantomData<fn() -> T>,
1390}
1391
1392impl<'de, T> DeserializeSeed<'de> for ProfileTopLevelElementSeed<'_, T>
1393where
1394 T: Deserialize<'de>,
1395{
1396 type Value = ProfileTopLevelElement<T>;
1397
1398 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1399 where
1400 D: Deserializer<'de>,
1401 {
1402 if self.rows.admit_top_level() {
1403 T::deserialize(deserializer).map(ProfileTopLevelElement::Value)
1404 } else {
1405 IgnoredAny::deserialize(deserializer).map(|_| ProfileTopLevelElement::Skipped)
1406 }
1407 }
1408}
1409
1410struct ProfileTopLevelSequenceSeed<'a, T> {
1411 rows: &'a mut ProfileRows,
1412 element: PhantomData<fn() -> T>,
1413}
1414
1415impl<'de, T> DeserializeSeed<'de> for ProfileTopLevelSequenceSeed<'_, T>
1416where
1417 T: Deserialize<'de>,
1418{
1419 type Value = CappedSequence<T>;
1420
1421 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1422 where
1423 D: Deserializer<'de>,
1424 {
1425 struct ProfileTopLevelSequenceVisitor<'a, T> {
1426 rows: &'a mut ProfileRows,
1427 element: PhantomData<fn() -> T>,
1428 }
1429
1430 impl<'de, T> Visitor<'de> for ProfileTopLevelSequenceVisitor<'_, T>
1431 where
1432 T: Deserialize<'de>,
1433 {
1434 type Value = CappedSequence<T>;
1435
1436 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1437 formatter.write_str("a bounded sequence of engine profile rows")
1438 }
1439
1440 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1441 where
1442 A: SeqAccess<'de>,
1443 {
1444 let mut values = Vec::with_capacity(
1445 sequence
1446 .size_hint()
1447 .unwrap_or(0)
1448 .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
1449 );
1450 let mut seen = 0usize;
1451 while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
1452 let Some(element) = sequence.next_element_seed(ProfileTopLevelElementSeed {
1453 rows: self.rows,
1454 element: PhantomData,
1455 })?
1456 else {
1457 return Ok(CappedSequence {
1458 values,
1459 overflowed: false,
1460 });
1461 };
1462 seen += 1;
1463 match element {
1464 ProfileTopLevelElement::Value(value) => values.push(value),
1465 ProfileTopLevelElement::Skipped => {
1466 let overflowed = consume_ignored_tail(
1467 &mut sequence,
1468 seen,
1469 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1470 )?;
1471 return Ok(CappedSequence { values, overflowed });
1472 }
1473 }
1474 }
1475 let overflowed = consume_ignored_tail(
1476 &mut sequence,
1477 seen,
1478 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1479 )?;
1480 Ok(CappedSequence { values, overflowed })
1481 }
1482 }
1483
1484 deserializer.deserialize_seq(ProfileTopLevelSequenceVisitor {
1485 rows: self.rows,
1486 element: PhantomData,
1487 })
1488 }
1489}
1490
1491enum PrimarySourceElement {
1492 Value(EnginePrimarySourceWireV1),
1493 Skipped,
1494}
1495
1496struct PrimarySourceElementSeed<'a> {
1497 rows: &'a mut ProfileRows,
1498}
1499
1500impl<'de> DeserializeSeed<'de> for PrimarySourceElementSeed<'_> {
1501 type Value = PrimarySourceElement;
1502
1503 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1504 where
1505 D: Deserializer<'de>,
1506 {
1507 if self.rows.admit_top_level() {
1508 EnginePrimarySourceSeed { rows: self.rows }
1509 .deserialize(deserializer)
1510 .map(PrimarySourceElement::Value)
1511 } else {
1512 IgnoredAny::deserialize(deserializer).map(|_| PrimarySourceElement::Skipped)
1513 }
1514 }
1515}
1516
1517struct PrimarySourcesSeed<'a> {
1518 rows: &'a mut ProfileRows,
1519}
1520
1521impl<'de> DeserializeSeed<'de> for PrimarySourcesSeed<'_> {
1522 type Value = CappedSequence<EnginePrimarySourceWireV1>;
1523
1524 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1525 where
1526 D: Deserializer<'de>,
1527 {
1528 struct PrimarySourcesVisitor<'a> {
1529 rows: &'a mut ProfileRows,
1530 }
1531
1532 impl<'de> Visitor<'de> for PrimarySourcesVisitor<'_> {
1533 type Value = CappedSequence<EnginePrimarySourceWireV1>;
1534
1535 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1536 formatter.write_str("a bounded sequence of engine primary sources")
1537 }
1538
1539 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1540 where
1541 A: SeqAccess<'de>,
1542 {
1543 let mut values = Vec::with_capacity(
1544 sequence
1545 .size_hint()
1546 .unwrap_or(0)
1547 .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
1548 );
1549 let mut seen = 0usize;
1550 while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
1551 let Some(element) =
1552 sequence.next_element_seed(PrimarySourceElementSeed { rows: self.rows })?
1553 else {
1554 return Ok(CappedSequence {
1555 values,
1556 overflowed: false,
1557 });
1558 };
1559 seen += 1;
1560 match element {
1561 PrimarySourceElement::Value(value) => values.push(value),
1562 PrimarySourceElement::Skipped => {
1563 let overflowed = consume_ignored_tail(
1564 &mut sequence,
1565 seen,
1566 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1567 )?;
1568 return Ok(CappedSequence { values, overflowed });
1569 }
1570 }
1571 }
1572 let overflowed = consume_ignored_tail(
1573 &mut sequence,
1574 seen,
1575 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1576 )?;
1577 Ok(CappedSequence { values, overflowed })
1578 }
1579 }
1580
1581 deserializer.deserialize_seq(PrimarySourcesVisitor { rows: self.rows })
1582 }
1583}
1584
1585struct ResolvedEngineProfileWireSeed {
1586 provenance_limit: Option<usize>,
1587}
1588
1589impl<'de> DeserializeSeed<'de> for ResolvedEngineProfileWireSeed {
1590 type Value = ResolvedEngineProfileWireV1;
1591
1592 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1593 where
1594 D: Deserializer<'de>,
1595 {
1596 #[derive(Deserialize)]
1597 #[serde(field_identifier, rename_all = "snake_case")]
1598 enum Field {
1599 Schema,
1600 Selection,
1601 FactBundleUrn,
1602 Identity,
1603 Facts,
1604 SettingDescriptors,
1605 PrimarySources,
1606 }
1607
1608 struct ProfileVisitor {
1609 provenance_limit: Option<usize>,
1610 }
1611
1612 impl<'de> Visitor<'de> for ProfileVisitor {
1613 type Value = ResolvedEngineProfileWireV1;
1614
1615 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1616 formatter.write_str("a resolved engine profile")
1617 }
1618
1619 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1620 where
1621 A: MapAccess<'de>,
1622 {
1623 let mut rows = ProfileRows::new(self.provenance_limit);
1624 let mut schema = None;
1625 let mut selection = None;
1626 let mut fact_bundle_urn = None;
1627 let mut identity = None;
1628 let mut facts = None;
1629 let mut setting_descriptors = None;
1630 let mut primary_sources = None;
1631 while let Some(field) = map.next_key()? {
1632 match field {
1633 Field::Schema => set_once(&mut schema, map.next_value()?, "schema")?,
1634 Field::Selection => {
1635 set_once(&mut selection, map.next_value()?, "selection")?
1636 }
1637 Field::FactBundleUrn => {
1638 set_once(&mut fact_bundle_urn, map.next_value()?, "fact_bundle_urn")?
1639 }
1640 Field::Identity => set_once(&mut identity, map.next_value()?, "identity")?,
1641 Field::Facts => {
1642 if facts.is_some() {
1643 return Err(A::Error::duplicate_field("facts"));
1644 }
1645 facts = Some(map.next_value_seed(ProfileTopLevelSequenceSeed {
1646 rows: &mut rows,
1647 element: PhantomData,
1648 })?);
1649 }
1650 Field::SettingDescriptors => {
1651 if setting_descriptors.is_some() {
1652 return Err(A::Error::duplicate_field("setting_descriptors"));
1653 }
1654 setting_descriptors =
1655 Some(map.next_value_seed(ProfileTopLevelSequenceSeed {
1656 rows: &mut rows,
1657 element: PhantomData,
1658 })?);
1659 }
1660 Field::PrimarySources => {
1661 if primary_sources.is_some() {
1662 return Err(A::Error::duplicate_field("primary_sources"));
1663 }
1664 primary_sources =
1665 Some(map.next_value_seed(PrimarySourcesSeed { rows: &mut rows })?);
1666 }
1667 }
1668 }
1669 Ok(ResolvedEngineProfileWireV1 {
1670 schema: required(schema, "schema")?,
1671 selection: required(selection, "selection")?,
1672 fact_bundle_urn: required(fact_bundle_urn, "fact_bundle_urn")?,
1673 identity: required(identity, "identity")?,
1674 facts: required(facts, "facts")?,
1675 setting_descriptors: required(setting_descriptors, "setting_descriptors")?,
1676 primary_sources: required(primary_sources, "primary_sources")?,
1677 provenance_rows_overflowed: rows.provenance_overflowed(),
1678 aggregate_rows: rows.local,
1679 })
1680 }
1681 }
1682
1683 deserializer.deserialize_struct(
1684 "ResolvedEngineProfileV1",
1685 &[
1686 "schema",
1687 "selection",
1688 "fact_bundle_urn",
1689 "identity",
1690 "facts",
1691 "setting_descriptors",
1692 "primary_sources",
1693 ],
1694 ProfileVisitor {
1695 provenance_limit: self.provenance_limit,
1696 },
1697 )
1698 }
1699}
1700
1701impl<'de> Deserialize<'de> for ResolvedEngineProfileWireV1 {
1702 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1703 where
1704 D: Deserializer<'de>,
1705 {
1706 ResolvedEngineProfileWireSeed {
1707 provenance_limit: None,
1708 }
1709 .deserialize(deserializer)
1710 }
1711}
1712
1713#[derive(Debug)]
1714pub(crate) enum EngineContractDecodeError {
1715 Shape(serde_json::Error),
1716 Semantic(EngineContractError),
1717}
1718
1719impl ResolvedEngineProfileV1 {
1720 fn validate_wire_limits(wire: &ResolvedEngineProfileWireV1) -> Result<(), EngineContractError> {
1721 validate_schema("profile.schema", &wire.schema, ENGINE_PROFILE_FACTS_V1_ID)?;
1722 wire.selection.validate()?;
1723 validate_required_text("profile.fact_bundle_urn", &wire.fact_bundle_urn)?;
1724 for (field, overflowed) in [
1725 ("profile.facts", wire.facts.overflowed),
1726 (
1727 "profile.setting_descriptors",
1728 wire.setting_descriptors.overflowed,
1729 ),
1730 ("profile.primary_sources", wire.primary_sources.overflowed),
1731 ] {
1732 if overflowed {
1733 return Err(EngineContractError::TooManyRows {
1734 field,
1735 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
1736 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1737 });
1738 }
1739 }
1740 for source in &wire.primary_sources.values {
1741 if source.supported_fact_ids.overflowed {
1742 return Err(EngineContractError::TooManyRows {
1743 field: "primary_sources.supported_fact_ids",
1744 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
1745 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1746 });
1747 }
1748 if source.supported_setting_ids.overflowed {
1749 return Err(EngineContractError::TooManyRows {
1750 field: "primary_sources.supported_setting_ids",
1751 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
1752 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
1753 });
1754 }
1755 }
1756 if wire.aggregate_rows.overflowed() {
1757 return Err(EngineContractError::TooManyAggregateRows {
1758 found: wire.aggregate_rows.found(),
1759 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
1760 });
1761 }
1762 Ok(())
1763 }
1764
1765 fn from_wire(wire: ResolvedEngineProfileWireV1) -> Result<Self, EngineContractError> {
1766 Self::validate_wire_limits(&wire)?;
1767 let primary_sources = wire
1768 .primary_sources
1769 .values
1770 .into_iter()
1771 .map(EnginePrimarySourceV1::from_wire)
1772 .collect::<Result<Vec<_>, _>>()?;
1773 let profile = Self {
1774 schema: wire.schema,
1775 selection: wire.selection,
1776 fact_bundle_urn: wire.fact_bundle_urn,
1777 identity: wire.identity,
1778 facts: wire.facts.values,
1779 setting_descriptors: wire.setting_descriptors.values,
1780 primary_sources,
1781 };
1782 profile.validate()?;
1783 Ok(profile)
1784 }
1785}
1786
1787#[cfg(test)]
1788pub(crate) fn decode_resolved_engine_profile_v1(
1789 raw: &str,
1790) -> Result<ResolvedEngineProfileV1, EngineContractDecodeError> {
1791 let wire = serde_json::from_str(raw).map_err(|source| {
1792 if source
1793 .to_string()
1794 .starts_with(&EngineContractError::InvalidAcceptedInputs.to_string())
1795 {
1796 EngineContractDecodeError::Semantic(EngineContractError::InvalidAcceptedInputs)
1797 } else {
1798 EngineContractDecodeError::Shape(source)
1799 }
1800 })?;
1801 ResolvedEngineProfileV1::from_wire(wire).map_err(EngineContractDecodeError::Semantic)
1802}
1803
1804pub(crate) enum EngineProfileLimitedDecodeError {
1805 Contract(EngineContractDecodeError),
1806 ProvenanceRowsOverflow,
1807}
1808
1809pub(crate) fn decode_resolved_engine_profile_v1_with_provenance_limit(
1810 raw: &str,
1811 provenance_limit: usize,
1812) -> Result<ResolvedEngineProfileV1, EngineProfileLimitedDecodeError> {
1813 let mut deserializer = serde_json::Deserializer::from_str(raw);
1814 let wire = ResolvedEngineProfileWireSeed {
1815 provenance_limit: Some(provenance_limit),
1816 }
1817 .deserialize(&mut deserializer)
1818 .map_err(|source| {
1819 if source
1820 .to_string()
1821 .starts_with(&EngineContractError::InvalidAcceptedInputs.to_string())
1822 {
1823 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(
1824 EngineContractError::InvalidAcceptedInputs,
1825 ))
1826 } else {
1827 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
1828 }
1829 })?;
1830 deserializer.end().map_err(|source| {
1831 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
1832 })?;
1833 ResolvedEngineProfileV1::validate_wire_limits(&wire).map_err(|source| {
1834 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
1835 })?;
1836 if wire.provenance_rows_overflowed {
1837 return Err(EngineProfileLimitedDecodeError::ProvenanceRowsOverflow);
1838 }
1839 ResolvedEngineProfileV1::from_wire(wire).map_err(|source| {
1840 EngineProfileLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
1841 })
1842}
1843
1844impl<'de> Deserialize<'de> for ResolvedEngineProfileV1 {
1845 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1846 where
1847 D: Deserializer<'de>,
1848 {
1849 Self::from_wire(ResolvedEngineProfileWireV1::deserialize(deserializer)?)
1850 .map_err(D::Error::custom)
1851 }
1852}
1853
1854#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1856#[serde(rename_all = "snake_case")]
1857pub enum EngineBakeOrExtractV1 {
1858 Bake,
1860 Extract,
1862}
1863
1864#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1866#[serde(rename_all = "snake_case")]
1867pub enum EngineSettingValueV1 {
1868 Boolean(bool),
1870 BakeOrExtract(EngineBakeOrExtractV1),
1872 SourceTransformPath(String),
1874}
1875
1876#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1878#[serde(deny_unknown_fields)]
1879pub struct EngineSettingRowV1 {
1880 id: EngineSettingIdV1,
1881 value: EngineSettingValueV1,
1882}
1883
1884impl EngineSettingRowV1 {
1885 pub const fn new(id: EngineSettingIdV1, value: EngineSettingValueV1) -> Self {
1887 Self { id, value }
1888 }
1889
1890 pub const fn id(&self) -> EngineSettingIdV1 {
1892 self.id
1893 }
1894
1895 pub const fn value(&self) -> &EngineSettingValueV1 {
1897 &self.value
1898 }
1899}
1900
1901#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1903#[serde(deny_unknown_fields)]
1904pub struct EngineClipSettingsV1 {
1905 clip_name: String,
1906 settings: Vec<EngineSettingRowV1>,
1907}
1908
1909#[derive(Deserialize)]
1910#[serde(deny_unknown_fields)]
1911struct EngineClipSettingsWireV1 {
1912 clip_name: String,
1913 #[serde(deserialize_with = "deserialize_collection_rows")]
1914 settings: CappedSequence<EngineSettingRowV1>,
1915}
1916
1917struct EngineClipSettingsSeed<'a> {
1918 rows: &'a mut SettingsRows,
1919}
1920
1921enum SettingsElement<T> {
1922 Value(T),
1923 Skipped,
1924}
1925
1926struct SettingsElementSeed<'a, T> {
1927 rows: &'a mut SettingsRows,
1928 element: PhantomData<fn() -> T>,
1929}
1930
1931impl<'de, T> DeserializeSeed<'de> for SettingsElementSeed<'_, T>
1932where
1933 T: Deserialize<'de>,
1934{
1935 type Value = SettingsElement<T>;
1936
1937 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1938 where
1939 D: Deserializer<'de>,
1940 {
1941 if self.rows.admit_setting() {
1942 T::deserialize(deserializer).map(SettingsElement::Value)
1943 } else {
1944 IgnoredAny::deserialize(deserializer).map(|_| SettingsElement::Skipped)
1945 }
1946 }
1947}
1948
1949struct SettingsSequenceSeed<'a, T> {
1950 rows: &'a mut SettingsRows,
1951 element: PhantomData<fn() -> T>,
1952}
1953
1954impl<'de, T> DeserializeSeed<'de> for SettingsSequenceSeed<'_, T>
1955where
1956 T: Deserialize<'de>,
1957{
1958 type Value = CappedSequence<T>;
1959
1960 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1961 where
1962 D: Deserializer<'de>,
1963 {
1964 struct SettingsSequenceVisitor<'a, T> {
1965 rows: &'a mut SettingsRows,
1966 element: PhantomData<fn() -> T>,
1967 }
1968
1969 impl<'de, T> Visitor<'de> for SettingsSequenceVisitor<'_, T>
1970 where
1971 T: Deserialize<'de>,
1972 {
1973 type Value = CappedSequence<T>;
1974
1975 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1976 formatter.write_str("a bounded sequence of engine setting rows")
1977 }
1978
1979 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1980 where
1981 A: SeqAccess<'de>,
1982 {
1983 let mut values = Vec::with_capacity(
1984 sequence
1985 .size_hint()
1986 .unwrap_or(0)
1987 .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
1988 );
1989 let mut seen = 0usize;
1990 while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
1991 let Some(element) = sequence.next_element_seed(SettingsElementSeed {
1992 rows: self.rows,
1993 element: PhantomData,
1994 })?
1995 else {
1996 return Ok(CappedSequence {
1997 values,
1998 overflowed: false,
1999 });
2000 };
2001 seen += 1;
2002 match element {
2003 SettingsElement::Value(value) => values.push(value),
2004 SettingsElement::Skipped => {
2005 let overflowed = consume_ignored_tail(
2006 &mut sequence,
2007 seen,
2008 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2009 )?;
2010 return Ok(CappedSequence { values, overflowed });
2011 }
2012 }
2013 }
2014 let overflowed = consume_ignored_tail(
2015 &mut sequence,
2016 seen,
2017 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2018 )?;
2019 Ok(CappedSequence { values, overflowed })
2020 }
2021 }
2022
2023 deserializer.deserialize_seq(SettingsSequenceVisitor {
2024 rows: self.rows,
2025 element: PhantomData,
2026 })
2027 }
2028}
2029
2030impl<'de> DeserializeSeed<'de> for EngineClipSettingsSeed<'_> {
2031 type Value = EngineClipSettingsWireV1;
2032
2033 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2034 where
2035 D: Deserializer<'de>,
2036 {
2037 #[derive(Deserialize)]
2038 #[serde(field_identifier, rename_all = "snake_case")]
2039 enum Field {
2040 ClipName,
2041 Settings,
2042 }
2043
2044 struct ClipSettingsVisitor<'a> {
2045 rows: &'a mut SettingsRows,
2046 }
2047
2048 impl<'de> Visitor<'de> for ClipSettingsVisitor<'_> {
2049 type Value = EngineClipSettingsWireV1;
2050
2051 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2052 formatter.write_str("an engine clip-settings record")
2053 }
2054
2055 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2056 where
2057 A: MapAccess<'de>,
2058 {
2059 let mut clip_name = None;
2060 let mut settings = None;
2061 while let Some(field) = map.next_key()? {
2062 match field {
2063 Field::ClipName => {
2064 set_once(&mut clip_name, map.next_value()?, "clip_name")?
2065 }
2066 Field::Settings => {
2067 if settings.is_some() {
2068 return Err(A::Error::duplicate_field("settings"));
2069 }
2070 settings = Some(map.next_value_seed(SettingsSequenceSeed {
2071 rows: self.rows,
2072 element: PhantomData,
2073 })?);
2074 }
2075 }
2076 }
2077 Ok(EngineClipSettingsWireV1 {
2078 clip_name: required(clip_name, "clip_name")?,
2079 settings: required(settings, "settings")?,
2080 })
2081 }
2082 }
2083
2084 deserializer.deserialize_struct(
2085 "EngineClipSettingsV1",
2086 &["clip_name", "settings"],
2087 ClipSettingsVisitor { rows: self.rows },
2088 )
2089 }
2090}
2091
2092impl EngineClipSettingsV1 {
2093 fn from_wire(wire: EngineClipSettingsWireV1) -> Result<Self, EngineContractError> {
2094 validate_text("settings.clips.clip_name", &wire.clip_name)?;
2095 if wire.settings.overflowed {
2096 return Err(EngineContractError::TooManyRows {
2097 field: "settings.clips.settings",
2098 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
2099 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2100 });
2101 }
2102 let clip = Self {
2103 clip_name: wire.clip_name,
2104 settings: wire.settings.values,
2105 };
2106 clip.validate(true)?;
2107 Ok(clip)
2108 }
2109}
2110
2111impl<'de> Deserialize<'de> for EngineClipSettingsV1 {
2112 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2113 where
2114 D: Deserializer<'de>,
2115 {
2116 Self::from_wire(EngineClipSettingsWireV1::deserialize(deserializer)?)
2117 .map_err(D::Error::custom)
2118 }
2119}
2120
2121impl EngineClipSettingsV1 {
2122 pub fn new(
2129 clip_name: impl Into<String>,
2130 mut settings: Vec<EngineSettingRowV1>,
2131 ) -> Result<Self, EngineContractError> {
2132 settings.sort_by_key(|row| row.id.as_str());
2133 let row = Self {
2134 clip_name: clip_name.into(),
2135 settings,
2136 };
2137 row.validate(true)?;
2138 Ok(row)
2139 }
2140
2141 pub fn clip_name(&self) -> &str {
2143 &self.clip_name
2144 }
2145
2146 pub fn settings(&self) -> &[EngineSettingRowV1] {
2148 &self.settings
2149 }
2150
2151 pub fn setting(&self, id: EngineSettingIdV1) -> Option<&EngineSettingValueV1> {
2153 self.settings
2154 .iter()
2155 .find(|row| row.id == id)
2156 .map(|row| &row.value)
2157 }
2158
2159 fn validate(&self, require_order: bool) -> Result<(), EngineContractError> {
2160 validate_text("settings.clips.clip_name", &self.clip_name)?;
2161 validate_collection_len("settings.clips.settings", self.settings.len())?;
2162 validate_unique_order(
2163 "settings.clips.settings",
2164 &self.settings,
2165 |row| row.id.as_str(),
2166 require_order,
2167 )?;
2168 for row in &self.settings {
2169 validate_setting_value(&row.value)?;
2170 }
2171 Ok(())
2172 }
2173
2174 fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
2175 let paths = self.settings.iter().filter_map(|row| match &row.value {
2176 EngineSettingValueV1::SourceTransformPath(path) => Some(path.len()),
2177 _ => None,
2178 });
2179 checked_sum(
2180 "settings retained text",
2181 [self.clip_name.len()].into_iter().chain(paths),
2182 )
2183 }
2184}
2185
2186#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2188pub struct ResolvedEngineSettingsV1 {
2189 schema: String,
2190 identity: InputIdentity,
2191 document_settings: Vec<EngineSettingRowV1>,
2192 clips: Vec<EngineClipSettingsV1>,
2193}
2194
2195impl ResolvedEngineSettingsV1 {
2196 pub fn new(
2205 profile: &ResolvedEngineProfileV1,
2206 mut document_settings: Vec<EngineSettingRowV1>,
2207 mut clips: Vec<EngineClipSettingsV1>,
2208 ) -> Result<Self, EngineContractError> {
2209 profile.validate()?;
2210 document_settings.sort_by_key(|row| row.id.as_str());
2211 clips.sort_by(|left, right| left.clip_name.cmp(&right.clip_name));
2212 let mut settings = Self {
2213 schema: RESOLVED_ENGINE_SETTINGS_V1_ID.to_owned(),
2214 identity: InputIdentity::from_bytes(&[]),
2215 document_settings,
2216 clips,
2217 };
2218 settings.validate_structure(true)?;
2219 settings.validate_materialization(profile, false)?;
2220 settings.identity = settings.computed_identity(profile);
2221 Ok(settings)
2222 }
2223
2224 pub fn contract_id(&self) -> &str {
2226 &self.schema
2227 }
2228
2229 pub const fn settings_identity(&self) -> &InputIdentity {
2231 &self.identity
2232 }
2233
2234 pub fn document_settings(&self) -> &[EngineSettingRowV1] {
2236 &self.document_settings
2237 }
2238
2239 pub fn clips(&self) -> &[EngineClipSettingsV1] {
2241 &self.clips
2242 }
2243
2244 pub fn document_setting(&self, id: EngineSettingIdV1) -> Option<&EngineSettingValueV1> {
2246 self.document_settings
2247 .iter()
2248 .find(|row| row.id == id)
2249 .map(|row| &row.value)
2250 }
2251
2252 pub fn clip_row(&self, ordinal: usize, clip_name: &str) -> Option<&EngineClipSettingsV1> {
2254 self.clips
2255 .get(ordinal)
2256 .filter(|row| row.clip_name == clip_name)
2257 }
2258
2259 pub fn validate_against(
2266 &self,
2267 profile: &ResolvedEngineProfileV1,
2268 ) -> Result<(), EngineContractError> {
2269 profile.validate()?;
2270 self.validate_structure(true)?;
2271 self.validate_materialization(profile, true)
2272 }
2273
2274 pub(crate) fn encode_preimage(
2276 &self,
2277 profile: &ResolvedEngineProfileV1,
2278 encoder: &mut CanonicalEncoder,
2279 ) {
2280 encoder.token(ENGINE_SETTINGS_PREIMAGE_DOMAIN);
2281 encode_profile_key(encoder, &profile.selection);
2282 encoder.field("fact_bundle_urn");
2283 encoder.token(&profile.fact_bundle_urn);
2284 encoder.field("document_settings");
2285 encoder.count(self.document_settings.len());
2286 for row in &self.document_settings {
2287 encoder.token(row.id.as_str());
2288 encode_setting_value(encoder, &row.value);
2289 }
2290 encoder.field("clips");
2291 encoder.count(self.clips.len());
2292 for clip in &self.clips {
2293 encoder.token(&clip.clip_name);
2294 encoder.count(clip.settings.len());
2295 for row in &clip.settings {
2296 encoder.token(row.id.as_str());
2297 encode_setting_value(encoder, &row.value);
2298 }
2299 }
2300 }
2301
2302 pub(crate) fn retained_rows(&self) -> Result<usize, EngineContractError> {
2303 checked_sum(
2304 "settings retained rows",
2305 [self.document_settings.len(), self.clips.len()]
2306 .into_iter()
2307 .chain(self.clips.iter().map(|clip| clip.settings.len())),
2308 )
2309 }
2310
2311 pub(crate) fn retained_text_bytes(&self) -> Result<usize, EngineContractError> {
2312 let document_paths = self
2313 .document_settings
2314 .iter()
2315 .filter_map(|row| match &row.value {
2316 EngineSettingValueV1::SourceTransformPath(path) => Some(path.len()),
2317 _ => None,
2318 });
2319 checked_sum_results(
2320 "settings retained text",
2321 document_paths,
2322 self.clips
2323 .iter()
2324 .map(EngineClipSettingsV1::retained_text_bytes),
2325 )
2326 }
2327
2328 fn computed_identity(&self, profile: &ResolvedEngineProfileV1) -> InputIdentity {
2329 let mut encoder = CanonicalEncoder::default();
2330 self.encode_preimage(profile, &mut encoder);
2331 encoder.identity()
2332 }
2333
2334 fn validate_structure(&self, require_order: bool) -> Result<(), EngineContractError> {
2335 validate_schema(
2336 "settings.schema",
2337 &self.schema,
2338 RESOLVED_ENGINE_SETTINGS_V1_ID,
2339 )?;
2340 validate_collection_len("settings.document_settings", self.document_settings.len())?;
2341 validate_collection_len("settings.clips", self.clips.len())?;
2342 validate_unique_order(
2343 "settings.document_settings",
2344 &self.document_settings,
2345 |row| row.id.as_str(),
2346 require_order,
2347 )?;
2348 for row in &self.document_settings {
2349 validate_setting_value(&row.value)?;
2350 }
2351 if require_order
2352 && !self
2353 .clips
2354 .windows(2)
2355 .all(|pair| pair[0].clip_name <= pair[1].clip_name)
2356 {
2357 return Err(EngineContractError::NonCanonicalOrder {
2358 field: "settings.clips",
2359 });
2360 }
2361 for clip in &self.clips {
2362 clip.validate(require_order)?;
2363 }
2364 let rows = self.retained_rows()?;
2365 if rows > ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS {
2366 return Err(EngineContractError::TooManyAggregateRows {
2367 found: rows,
2368 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
2369 });
2370 }
2371 let text = self.retained_text_bytes()?;
2372 if text > ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES {
2373 return Err(EngineContractError::TooMuchAggregateText {
2374 found: text,
2375 max: ENGINE_CONTRACT_V1_MAX_TOTAL_TEXT_BYTES,
2376 });
2377 }
2378 Ok(())
2379 }
2380
2381 fn validate_materialization(
2382 &self,
2383 profile: &ResolvedEngineProfileV1,
2384 verify_identity: bool,
2385 ) -> Result<(), EngineContractError> {
2386 validate_rows_for_scope(
2387 profile,
2388 &self.document_settings,
2389 EngineSettingScopeV1::Document,
2390 "document",
2391 )?;
2392 for (ordinal, clip) in self.clips.iter().enumerate() {
2393 validate_rows_for_scope(
2394 profile,
2395 &clip.settings,
2396 EngineSettingScopeV1::Clip,
2397 &format!("clip[{ordinal}]"),
2398 )?;
2399 }
2400 if verify_identity && self.identity != self.computed_identity(profile) {
2401 return Err(EngineContractError::IdentityMismatch {
2402 contract: RESOLVED_ENGINE_SETTINGS_V1_ID,
2403 });
2404 }
2405 Ok(())
2406 }
2407}
2408
2409struct ResolvedEngineSettingsWireV1 {
2410 schema: String,
2411 identity: InputIdentity,
2412 document_settings: CappedSequence<EngineSettingRowV1>,
2413 clips: CappedSequence<EngineClipSettingsWireV1>,
2414 aggregate_rows: RowBudget,
2415 provenance_rows_overflowed: bool,
2416}
2417
2418enum ClipSettingsElement {
2419 Value(EngineClipSettingsWireV1),
2420 Skipped,
2421}
2422
2423struct ClipSettingsElementSeed<'a> {
2424 rows: &'a mut SettingsRows,
2425}
2426
2427impl<'de> DeserializeSeed<'de> for ClipSettingsElementSeed<'_> {
2428 type Value = ClipSettingsElement;
2429
2430 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2431 where
2432 D: Deserializer<'de>,
2433 {
2434 if self.rows.admit_clip() {
2435 EngineClipSettingsSeed { rows: self.rows }
2436 .deserialize(deserializer)
2437 .map(ClipSettingsElement::Value)
2438 } else {
2439 IgnoredAny::deserialize(deserializer).map(|_| ClipSettingsElement::Skipped)
2440 }
2441 }
2442}
2443
2444struct ClipSettingsSequenceSeed<'a> {
2445 rows: &'a mut SettingsRows,
2446}
2447
2448impl<'de> DeserializeSeed<'de> for ClipSettingsSequenceSeed<'_> {
2449 type Value = CappedSequence<EngineClipSettingsWireV1>;
2450
2451 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2452 where
2453 D: Deserializer<'de>,
2454 {
2455 struct ClipSettingsSequenceVisitor<'a> {
2456 rows: &'a mut SettingsRows,
2457 }
2458
2459 impl<'de> Visitor<'de> for ClipSettingsSequenceVisitor<'_> {
2460 type Value = CappedSequence<EngineClipSettingsWireV1>;
2461
2462 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2463 formatter.write_str("a bounded sequence of engine clip settings")
2464 }
2465
2466 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
2467 where
2468 A: SeqAccess<'de>,
2469 {
2470 let mut values = Vec::with_capacity(
2471 sequence
2472 .size_hint()
2473 .unwrap_or(0)
2474 .min(ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS),
2475 );
2476 let mut seen = 0usize;
2477 while seen < ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
2478 let Some(element) =
2479 sequence.next_element_seed(ClipSettingsElementSeed { rows: self.rows })?
2480 else {
2481 return Ok(CappedSequence {
2482 values,
2483 overflowed: false,
2484 });
2485 };
2486 seen += 1;
2487 match element {
2488 ClipSettingsElement::Value(value) => values.push(value),
2489 ClipSettingsElement::Skipped => {
2490 let overflowed = consume_ignored_tail(
2491 &mut sequence,
2492 seen,
2493 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2494 )?;
2495 return Ok(CappedSequence { values, overflowed });
2496 }
2497 }
2498 }
2499 let overflowed = consume_ignored_tail(
2500 &mut sequence,
2501 seen,
2502 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2503 )?;
2504 Ok(CappedSequence { values, overflowed })
2505 }
2506 }
2507
2508 deserializer.deserialize_seq(ClipSettingsSequenceVisitor { rows: self.rows })
2509 }
2510}
2511
2512struct ResolvedEngineSettingsWireSeed {
2513 provenance_limit: Option<usize>,
2514}
2515
2516impl<'de> DeserializeSeed<'de> for ResolvedEngineSettingsWireSeed {
2517 type Value = ResolvedEngineSettingsWireV1;
2518
2519 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2520 where
2521 D: Deserializer<'de>,
2522 {
2523 #[derive(Deserialize)]
2524 #[serde(field_identifier, rename_all = "snake_case")]
2525 enum Field {
2526 Schema,
2527 Identity,
2528 DocumentSettings,
2529 Clips,
2530 }
2531
2532 struct SettingsVisitor {
2533 provenance_limit: Option<usize>,
2534 }
2535
2536 impl<'de> Visitor<'de> for SettingsVisitor {
2537 type Value = ResolvedEngineSettingsWireV1;
2538
2539 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2540 formatter.write_str("resolved engine settings")
2541 }
2542
2543 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2544 where
2545 A: MapAccess<'de>,
2546 {
2547 let mut rows = SettingsRows::new(self.provenance_limit);
2548 let mut schema = None;
2549 let mut identity = None;
2550 let mut document_settings = None;
2551 let mut clips = None;
2552 while let Some(field) = map.next_key()? {
2553 match field {
2554 Field::Schema => set_once(&mut schema, map.next_value()?, "schema")?,
2555 Field::Identity => set_once(&mut identity, map.next_value()?, "identity")?,
2556 Field::DocumentSettings => {
2557 if document_settings.is_some() {
2558 return Err(A::Error::duplicate_field("document_settings"));
2559 }
2560 document_settings =
2561 Some(map.next_value_seed(SettingsSequenceSeed {
2562 rows: &mut rows,
2563 element: PhantomData,
2564 })?);
2565 }
2566 Field::Clips => {
2567 if clips.is_some() {
2568 return Err(A::Error::duplicate_field("clips"));
2569 }
2570 clips =
2571 Some(map.next_value_seed(ClipSettingsSequenceSeed {
2572 rows: &mut rows,
2573 })?);
2574 }
2575 }
2576 }
2577 Ok(ResolvedEngineSettingsWireV1 {
2578 schema: required(schema, "schema")?,
2579 identity: required(identity, "identity")?,
2580 document_settings: required(document_settings, "document_settings")?,
2581 clips: required(clips, "clips")?,
2582 provenance_rows_overflowed: rows.provenance_overflowed(),
2583 aggregate_rows: rows.local,
2584 })
2585 }
2586 }
2587
2588 deserializer.deserialize_struct(
2589 "ResolvedEngineSettingsV1",
2590 &["schema", "identity", "document_settings", "clips"],
2591 SettingsVisitor {
2592 provenance_limit: self.provenance_limit,
2593 },
2594 )
2595 }
2596}
2597
2598impl<'de> Deserialize<'de> for ResolvedEngineSettingsWireV1 {
2599 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2600 where
2601 D: Deserializer<'de>,
2602 {
2603 ResolvedEngineSettingsWireSeed {
2604 provenance_limit: None,
2605 }
2606 .deserialize(deserializer)
2607 }
2608}
2609
2610impl ResolvedEngineSettingsV1 {
2611 fn validate_wire_limits(
2612 wire: &ResolvedEngineSettingsWireV1,
2613 ) -> Result<(), EngineContractError> {
2614 validate_schema(
2615 "settings.schema",
2616 &wire.schema,
2617 RESOLVED_ENGINE_SETTINGS_V1_ID,
2618 )?;
2619 for (field, overflowed) in [
2620 (
2621 "settings.document_settings",
2622 wire.document_settings.overflowed,
2623 ),
2624 ("settings.clips", wire.clips.overflowed),
2625 ] {
2626 if overflowed {
2627 return Err(EngineContractError::TooManyRows {
2628 field,
2629 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
2630 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2631 });
2632 }
2633 }
2634 for clip in &wire.clips.values {
2635 if clip.settings.overflowed {
2636 return Err(EngineContractError::TooManyRows {
2637 field: "settings.clips.settings",
2638 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
2639 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2640 });
2641 }
2642 }
2643 if wire.aggregate_rows.overflowed() {
2644 return Err(EngineContractError::TooManyAggregateRows {
2645 found: wire.aggregate_rows.found(),
2646 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
2647 });
2648 }
2649 Ok(())
2650 }
2651
2652 fn from_wire(wire: ResolvedEngineSettingsWireV1) -> Result<Self, EngineContractError> {
2653 Self::validate_wire_limits(&wire)?;
2654 let clips = wire
2655 .clips
2656 .values
2657 .into_iter()
2658 .map(EngineClipSettingsV1::from_wire)
2659 .collect::<Result<Vec<_>, _>>()?;
2660 let settings = Self {
2661 schema: wire.schema,
2662 identity: wire.identity,
2663 document_settings: wire.document_settings.values,
2664 clips,
2665 };
2666 settings.validate_structure(true)?;
2667 Ok(settings)
2668 }
2669}
2670
2671pub(crate) enum EngineSettingsLimitedDecodeError {
2672 Contract(EngineContractDecodeError),
2673 ProvenanceRowsOverflow,
2674}
2675
2676pub(crate) fn decode_resolved_engine_settings_v1_with_provenance_limit(
2677 raw: &str,
2678 provenance_limit: usize,
2679) -> Result<ResolvedEngineSettingsV1, EngineSettingsLimitedDecodeError> {
2680 let mut deserializer = serde_json::Deserializer::from_str(raw);
2681 let wire = ResolvedEngineSettingsWireSeed {
2682 provenance_limit: Some(provenance_limit),
2683 }
2684 .deserialize(&mut deserializer)
2685 .map_err(|source| {
2686 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
2687 })?;
2688 deserializer.end().map_err(|source| {
2689 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Shape(source))
2690 })?;
2691 ResolvedEngineSettingsV1::validate_wire_limits(&wire).map_err(|source| {
2692 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
2693 })?;
2694 if wire.provenance_rows_overflowed {
2695 return Err(EngineSettingsLimitedDecodeError::ProvenanceRowsOverflow);
2696 }
2697 ResolvedEngineSettingsV1::from_wire(wire).map_err(|source| {
2698 EngineSettingsLimitedDecodeError::Contract(EngineContractDecodeError::Semantic(source))
2699 })
2700}
2701
2702impl<'de> Deserialize<'de> for ResolvedEngineSettingsV1 {
2703 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2704 where
2705 D: Deserializer<'de>,
2706 {
2707 Self::from_wire(ResolvedEngineSettingsWireV1::deserialize(deserializer)?)
2708 .map_err(D::Error::custom)
2709 }
2710}
2711
2712#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2714#[non_exhaustive]
2715pub enum EngineContractError {
2716 #[error("{field} must be {expected:?}, found {found:?}")]
2718 InvalidSchema {
2719 field: &'static str,
2721 expected: &'static str,
2723 found: String,
2725 },
2726 #[error("{field} must not be empty")]
2728 EmptyText {
2729 field: &'static str,
2731 },
2732 #[error("{field} retains {found} UTF-8 bytes, exceeding {max}")]
2734 TextTooLong {
2735 field: &'static str,
2737 found: usize,
2739 max: usize,
2741 },
2742 #[error("{field} contains {found} rows, exceeding {max}")]
2744 TooManyRows {
2745 field: &'static str,
2747 found: usize,
2749 max: usize,
2751 },
2752 #[error("profile/settings retain {found} aggregate rows, exceeding {max}")]
2754 TooManyAggregateRows {
2755 found: usize,
2757 max: usize,
2759 },
2760 #[error("profile/settings retain {found} UTF-8 bytes, exceeding {max}")]
2762 TooMuchAggregateText {
2763 found: usize,
2765 max: usize,
2767 },
2768 #[error("checked arithmetic overflow while accounting {field}")]
2770 ArithmeticOverflow {
2771 field: &'static str,
2773 },
2774 #[error("{field} contains duplicate key {key:?}")]
2776 DuplicateKey {
2777 field: &'static str,
2779 key: String,
2781 },
2782 #[error("{field} is not in canonical order")]
2784 NonCanonicalOrder {
2785 field: &'static str,
2787 },
2788 #[error("profile facts must contain every V1 fact id exactly once")]
2790 InvalidFactInventory,
2791 #[error("profile fact {fact:?} carries an invalid known-value variant")]
2793 InvalidFactValue {
2794 fact: EngineFactIdV1,
2796 },
2797 #[error("profile accepted_inputs must be a nonempty canonical set")]
2799 InvalidAcceptedInputs,
2800 #[error("setting descriptor {setting:?} has inconsistent applicability/default status")]
2802 InvalidDescriptorDefault {
2803 setting: EngineSettingIdV1,
2805 },
2806 #[error("primary source {source_id:?} references absent fact {fact:?}")]
2808 UnknownSourceFact {
2809 source_id: String,
2811 fact: EngineFactIdV1,
2813 },
2814 #[error("primary source {source_id:?} references non-known fact {fact:?}")]
2816 SourceReferencesNonKnownFact {
2817 source_id: String,
2819 fact: EngineFactIdV1,
2821 },
2822 #[error("primary source {source_id:?} references absent setting {setting:?}")]
2824 UnknownSourceSetting {
2825 source_id: String,
2827 setting: EngineSettingIdV1,
2829 },
2830 #[error("known profile fact {fact:?} has no primary-source reference")]
2832 UnreferencedKnownFact {
2833 fact: EngineFactIdV1,
2835 },
2836 #[error("setting descriptor {setting:?} has no primary-source reference")]
2838 UnreferencedSetting {
2839 setting: EngineSettingIdV1,
2841 },
2842 #[error("source-transform path is invalid: {reason}")]
2844 InvalidSourceTransformPath {
2845 reason: &'static str,
2847 },
2848 #[error("{location} contains unknown setting {setting:?}")]
2850 UnknownMaterializedSetting {
2851 location: String,
2853 setting: EngineSettingIdV1,
2855 },
2856 #[error("{location} contains {setting:?} at the wrong scope")]
2858 WrongSettingScope {
2859 location: String,
2861 setting: EngineSettingIdV1,
2863 },
2864 #[error("{location} contains non-applicable setting {setting:?}")]
2866 NonApplicableSetting {
2867 location: String,
2869 setting: EngineSettingIdV1,
2871 },
2872 #[error("{location} contains {setting:?} with a value outside its domain")]
2874 WrongSettingDomain {
2875 location: String,
2877 setting: EngineSettingIdV1,
2879 },
2880 #[error("{location} is missing required setting {setting:?}")]
2882 MissingRequiredSetting {
2883 location: String,
2885 setting: EngineSettingIdV1,
2887 },
2888 #[error("identity does not match canonical {contract}")]
2890 IdentityMismatch {
2891 contract: &'static str,
2893 },
2894}
2895
2896fn validate_schema(
2897 field: &'static str,
2898 found: &str,
2899 expected: &'static str,
2900) -> Result<(), EngineContractError> {
2901 if found == expected {
2902 Ok(())
2903 } else {
2904 Err(EngineContractError::InvalidSchema {
2905 field,
2906 expected,
2907 found: found.to_owned(),
2908 })
2909 }
2910}
2911
2912fn validate_required_text(field: &'static str, value: &str) -> Result<(), EngineContractError> {
2913 if value.is_empty() {
2914 return Err(EngineContractError::EmptyText { field });
2915 }
2916 validate_text(field, value)
2917}
2918
2919fn validate_text(field: &'static str, value: &str) -> Result<(), EngineContractError> {
2920 if value.len() > ENGINE_CONTRACT_V1_MAX_TEXT_BYTES {
2921 Err(EngineContractError::TextTooLong {
2922 field,
2923 found: value.len(),
2924 max: ENGINE_CONTRACT_V1_MAX_TEXT_BYTES,
2925 })
2926 } else {
2927 Ok(())
2928 }
2929}
2930
2931fn validate_collection_len(field: &'static str, found: usize) -> Result<(), EngineContractError> {
2932 if found > ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
2933 Err(EngineContractError::TooManyRows {
2934 field,
2935 found,
2936 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
2937 })
2938 } else {
2939 Ok(())
2940 }
2941}
2942
2943fn validate_unique_order<T>(
2944 field: &'static str,
2945 rows: &[T],
2946 key: impl Fn(&T) -> &str,
2947 require_order: bool,
2948) -> Result<(), EngineContractError> {
2949 let mut seen = BTreeSet::new();
2950 let mut previous = None;
2951 for row in rows {
2952 let current = key(row);
2953 if !seen.insert(current) {
2954 return Err(EngineContractError::DuplicateKey {
2955 field,
2956 key: current.to_owned(),
2957 });
2958 }
2959 if require_order && previous.is_some_and(|previous| previous >= current) {
2960 return Err(EngineContractError::NonCanonicalOrder { field });
2961 }
2962 previous = Some(current);
2963 }
2964 Ok(())
2965}
2966
2967fn validate_fact_value(fact: &EngineProfileFactV1) -> Result<(), EngineContractError> {
2968 let EngineFactStateV1::Known(value) = &fact.state else {
2969 return Ok(());
2970 };
2971 let valid = matches!(
2972 (fact.id, value),
2973 (
2974 EngineFactIdV1::AcceptedInputs,
2975 EngineFactValueV1::AcceptedFormats(_)
2976 ) | (
2977 EngineFactIdV1::AnimationAddressability,
2978 EngineFactValueV1::AnimationAddressability(_)
2979 ) | (
2980 EngineFactIdV1::TargetCoordinateBasis,
2981 EngineFactValueV1::CoordinateBasis(_)
2982 ) | (
2983 EngineFactIdV1::TargetLinearUnit,
2984 EngineFactValueV1::LinearUnit(_)
2985 ) | (
2986 EngineFactIdV1::UnitConversionControl | EngineFactIdV1::AxisConversionControl,
2987 EngineFactValueV1::ConversionControl(_)
2988 ) | (
2989 EngineFactIdV1::WholeEndFrameRequired,
2990 EngineFactValueV1::Boolean(_)
2991 ) | (
2992 EngineFactIdV1::AnimationChannelHandling
2993 | EngineFactIdV1::ExtensionHandling
2994 | EngineFactIdV1::ConstructHandling,
2995 EngineFactValueV1::ImportHandling(_)
2996 ) | (
2997 EngineFactIdV1::AnimationTargetAddressability,
2998 EngineFactValueV1::TargetAddressability(_)
2999 ) | (
3000 EngineFactIdV1::RootMotionAddressability,
3001 EngineFactValueV1::RootMotionAddressability(_)
3002 )
3003 );
3004 if !valid {
3005 return Err(EngineContractError::InvalidFactValue { fact: fact.id });
3006 }
3007 if let EngineFactValueV1::AcceptedFormats(formats) = value
3008 && (formats.is_empty()
3009 || formats.len() > ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
3010 || !formats
3011 .windows(2)
3012 .all(|pair| source_format_name(pair[0]) < source_format_name(pair[1])))
3013 {
3014 return Err(EngineContractError::InvalidAcceptedInputs);
3015 }
3016 if let EngineFactValueV1::ConversionControl(EngineConversionControlV1::ProfileSetting(
3017 setting,
3018 )) = value
3019 {
3020 let expected = match fact.id {
3021 EngineFactIdV1::UnitConversionControl => EngineSettingIdV1::ConvertUnits,
3022 EngineFactIdV1::AxisConversionControl => EngineSettingIdV1::BakeAxisConversion,
3023 _ => return Err(EngineContractError::InvalidFactValue { fact: fact.id }),
3024 };
3025 if *setting != expected {
3026 return Err(EngineContractError::InvalidFactValue { fact: fact.id });
3027 }
3028 }
3029 Ok(())
3030}
3031
3032fn validate_setting_value(value: &EngineSettingValueV1) -> Result<(), EngineContractError> {
3033 let EngineSettingValueV1::SourceTransformPath(path) = value else {
3034 return Ok(());
3035 };
3036 validate_text("source_transform_path", path)?;
3037 let reason = if path.is_empty() {
3038 Some("empty path")
3039 } else if path.starts_with('/') {
3040 Some("absolute path")
3041 } else if path.chars().any(char::is_control) {
3042 Some("control character")
3043 } else if path.split('/').any(str::is_empty) {
3044 Some("empty path segment")
3045 } else if path.split('/').any(|segment| matches!(segment, "." | "..")) {
3046 Some("dot path segment")
3047 } else {
3048 None
3049 };
3050 if let Some(reason) = reason {
3051 Err(EngineContractError::InvalidSourceTransformPath { reason })
3052 } else {
3053 Ok(())
3054 }
3055}
3056
3057fn validate_rows_for_scope(
3058 profile: &ResolvedEngineProfileV1,
3059 rows: &[EngineSettingRowV1],
3060 scope: EngineSettingScopeV1,
3061 location: &str,
3062) -> Result<(), EngineContractError> {
3063 for row in rows {
3064 let Some(descriptor) = profile.setting_descriptor(row.id) else {
3065 return Err(EngineContractError::UnknownMaterializedSetting {
3066 location: location.to_owned(),
3067 setting: row.id,
3068 });
3069 };
3070 if descriptor.scope != scope {
3071 return Err(EngineContractError::WrongSettingScope {
3072 location: location.to_owned(),
3073 setting: row.id,
3074 });
3075 }
3076 if descriptor.applicability != EngineSettingApplicabilityV1::Applicable {
3077 return Err(EngineContractError::NonApplicableSetting {
3078 location: location.to_owned(),
3079 setting: row.id,
3080 });
3081 }
3082 let domain_matches = matches!(
3083 (descriptor.domain, &row.value),
3084 (
3085 EngineSettingDomainV1::Boolean,
3086 EngineSettingValueV1::Boolean(_)
3087 ) | (
3088 EngineSettingDomainV1::BakeOrExtract,
3089 EngineSettingValueV1::BakeOrExtract(_)
3090 ) | (
3091 EngineSettingDomainV1::SourceTransformPath,
3092 EngineSettingValueV1::SourceTransformPath(_)
3093 )
3094 );
3095 if !domain_matches {
3096 return Err(EngineContractError::WrongSettingDomain {
3097 location: location.to_owned(),
3098 setting: row.id,
3099 });
3100 }
3101 }
3102 for descriptor in &profile.setting_descriptors {
3103 if descriptor.scope == scope
3104 && descriptor.applicability == EngineSettingApplicabilityV1::Applicable
3105 && descriptor.default_status == EngineDefaultStatusV1::RequiredWithoutDefault
3106 && !rows.iter().any(|row| row.id == descriptor.id)
3107 {
3108 return Err(EngineContractError::MissingRequiredSetting {
3109 location: location.to_owned(),
3110 setting: descriptor.id,
3111 });
3112 }
3113 }
3114 Ok(())
3115}
3116
3117fn checked_sum(
3118 field: &'static str,
3119 values: impl IntoIterator<Item = usize>,
3120) -> Result<usize, EngineContractError> {
3121 values.into_iter().try_fold(0_usize, |total, value| {
3122 total
3123 .checked_add(value)
3124 .ok_or(EngineContractError::ArithmeticOverflow { field })
3125 })
3126}
3127
3128fn checked_sum_results(
3129 field: &'static str,
3130 initial: impl IntoIterator<Item = usize>,
3131 values: impl IntoIterator<Item = Result<usize, EngineContractError>>,
3132) -> Result<usize, EngineContractError> {
3133 let initial = checked_sum(field, initial)?;
3134 values.into_iter().try_fold(initial, |total, value| {
3135 total
3136 .checked_add(value?)
3137 .ok_or(EngineContractError::ArithmeticOverflow { field })
3138 })
3139}
3140
3141fn encode_profile_key(encoder: &mut CanonicalEncoder, selection: &EngineProfileSelectionV1) {
3142 encoder.field("selection");
3143 encoder.token(&selection.family);
3144 encoder.token(selection.profile_revision.to_string());
3145 encoder.token(&selection.engine_version);
3146 encoder.token(&selection.importer);
3147}
3148
3149fn encode_fact_state(encoder: &mut CanonicalEncoder, state: &EngineFactStateV1) {
3150 match state {
3151 EngineFactStateV1::Unknown => encoder.token("unknown"),
3152 EngineFactStateV1::NotApplicable => encoder.token("not_applicable"),
3153 EngineFactStateV1::Known(value) => {
3154 encoder.token("known");
3155 match value {
3156 EngineFactValueV1::AcceptedFormats(formats) => {
3157 encoder.token("accepted_formats");
3158 encoder.count(formats.len());
3159 for format in formats {
3160 encoder.token(source_format_name(*format));
3161 }
3162 }
3163 EngineFactValueV1::AnimationAddressability(value) => {
3164 encoder.token("animation_addressability");
3165 encoder.token(match value {
3166 EngineAnimationAddressabilityV1::GltfAssetLabel => "gltf_asset_label",
3167 });
3168 }
3169 EngineFactValueV1::CoordinateBasis(value) => {
3170 encoder.token("coordinate_basis");
3171 encoder.token(match value.handedness {
3172 EngineHandednessV1::Left => "left",
3173 EngineHandednessV1::Right => "right",
3174 });
3175 encoder.token(match value.up_axis {
3176 EngineUpAxisV1::X => "x",
3177 EngineUpAxisV1::Y => "y",
3178 EngineUpAxisV1::Z => "z",
3179 });
3180 encoder.token(match value.forward_axis {
3181 EngineForwardAxisV1::PositiveX => "+x",
3182 EngineForwardAxisV1::NegativeX => "-x",
3183 EngineForwardAxisV1::PositiveY => "+y",
3184 EngineForwardAxisV1::NegativeY => "-y",
3185 EngineForwardAxisV1::PositiveZ => "+z",
3186 EngineForwardAxisV1::NegativeZ => "-z",
3187 });
3188 }
3189 EngineFactValueV1::LinearUnit(value) => {
3190 encoder.token("linear_unit");
3191 encoder.token(match value {
3192 EngineLinearUnitV1::Metre => "metre",
3193 EngineLinearUnitV1::Centimetre => "centimetre",
3194 });
3195 }
3196 EngineFactValueV1::ConversionControl(value) => {
3197 encoder.token("conversion_control");
3198 match value {
3199 EngineConversionControlV1::ProfileSetting(setting) => {
3200 encoder.token("profile_setting");
3201 encoder.token(setting.as_str());
3202 }
3203 EngineConversionControlV1::ImporterOption => {
3204 encoder.token("importer_option");
3205 }
3206 }
3207 }
3208 EngineFactValueV1::Boolean(value) => {
3209 encoder.token("boolean");
3210 encoder.token(if *value { "true" } else { "false" });
3211 }
3212 EngineFactValueV1::ImportHandling(value) => {
3213 encoder.token("import_handling");
3214 encoder.token(match value {
3215 EngineImportHandlingV1::Preserved => "preserved",
3216 EngineImportHandlingV1::Converted => "converted",
3217 EngineImportHandlingV1::Discarded => "discarded",
3218 EngineImportHandlingV1::Unsupported => "unsupported",
3219 });
3220 }
3221 EngineFactValueV1::TargetAddressability(value) => {
3222 encoder.token("target_addressability");
3223 encoder.token(match value {
3224 EngineTargetAddressabilityV1::NamePathDerivedId => "name_path_derived_id",
3225 });
3226 }
3227 EngineFactValueV1::RootMotionAddressability(value) => {
3228 encoder.token("root_motion_addressability");
3229 encoder.token(match value {
3230 EngineRootMotionAddressabilityV1::ExactSourceTransformPath => {
3231 "exact_source_transform_path"
3232 }
3233 EngineRootMotionAddressabilityV1::HumanoidAvatarBody => {
3234 "humanoid_avatar_body"
3235 }
3236 });
3237 }
3238 }
3239 }
3240 }
3241}
3242
3243fn encode_setting_value(encoder: &mut CanonicalEncoder, value: &EngineSettingValueV1) {
3244 match value {
3245 EngineSettingValueV1::Boolean(value) => {
3246 encoder.token("boolean");
3247 encoder.token(if *value { "true" } else { "false" });
3248 }
3249 EngineSettingValueV1::BakeOrExtract(value) => {
3250 encoder.token("bake_or_extract");
3251 encoder.token(match value {
3252 EngineBakeOrExtractV1::Bake => "bake",
3253 EngineBakeOrExtractV1::Extract => "extract",
3254 });
3255 }
3256 EngineSettingValueV1::SourceTransformPath(value) => {
3257 encoder.token("source_transform_path");
3258 encoder.token(value);
3259 }
3260 }
3261}
3262
3263const fn source_format_name(format: SourceFormatV1) -> &'static str {
3264 match format {
3265 SourceFormatV1::GltfJson => "gltf_json",
3266 SourceFormatV1::Glb => "glb",
3267 SourceFormatV1::Fbx => "fbx",
3268 }
3269}
3270
3271const fn setting_scope_name(scope: EngineSettingScopeV1) -> &'static str {
3272 match scope {
3273 EngineSettingScopeV1::Document => "document",
3274 EngineSettingScopeV1::Clip => "clip",
3275 }
3276}
3277
3278const fn setting_domain_name(domain: EngineSettingDomainV1) -> &'static str {
3279 match domain {
3280 EngineSettingDomainV1::Boolean => "boolean",
3281 EngineSettingDomainV1::BakeOrExtract => "bake_or_extract",
3282 EngineSettingDomainV1::SourceTransformPath => "source_transform_path",
3283 }
3284}
3285
3286#[cfg(test)]
3287mod tests {
3288 use super::*;
3289 use serde_json::json;
3290
3291 fn fact_inventory(accepted: Vec<SourceFormatV1>) -> Vec<EngineProfileFactV1> {
3292 ALL_FACT_IDS
3293 .into_iter()
3294 .map(|id| {
3295 let state = if id == EngineFactIdV1::AcceptedInputs {
3296 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(accepted.clone()))
3297 } else {
3298 EngineFactStateV1::Unknown
3299 };
3300 EngineProfileFactV1::new(id, state)
3301 })
3302 .collect()
3303 }
3304
3305 fn godot_profile() -> ResolvedEngineProfileV1 {
3306 ResolvedEngineProfileV1::new(
3307 EngineProfileSelectionV1::new("godot", 1, "4.7", "resource-importer-scene").unwrap(),
3308 "urn:animsmith:engine-profile:godot:1",
3309 fact_inventory(vec![
3310 SourceFormatV1::GltfJson,
3311 SourceFormatV1::Glb,
3312 SourceFormatV1::Fbx,
3313 ]),
3314 vec![],
3315 vec![
3316 EnginePrimarySourceV1::new(
3317 "godot-resource-importer-scene-4.7",
3318 "4.7",
3319 "https://docs.godotengine.org/en/4.7/classes/class_resourceimporterscene.html",
3320 "2026-08-20",
3321 vec![EngineFactIdV1::AcceptedInputs],
3322 vec![],
3323 )
3324 .unwrap(),
3325 ],
3326 )
3327 .unwrap()
3328 }
3329
3330 fn settings_profile(family: &str) -> ResolvedEngineProfileV1 {
3331 let mut facts = fact_inventory(vec![SourceFormatV1::Fbx]);
3332 facts
3333 .iter_mut()
3334 .find(|fact| fact.id == EngineFactIdV1::UnitConversionControl)
3335 .unwrap()
3336 .state = EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
3337 EngineConversionControlV1::ProfileSetting(EngineSettingIdV1::ConvertUnits),
3338 ));
3339 facts
3340 .iter_mut()
3341 .find(|fact| fact.id == EngineFactIdV1::AxisConversionControl)
3342 .unwrap()
3343 .state = EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
3344 EngineConversionControlV1::ProfileSetting(EngineSettingIdV1::BakeAxisConversion),
3345 ));
3346 let descriptors = vec![
3347 EngineSettingDescriptorV1::new(
3348 EngineSettingIdV1::ConvertUnits,
3349 EngineSettingScopeV1::Document,
3350 EngineSettingDomainV1::Boolean,
3351 EngineSettingApplicabilityV1::Applicable,
3352 EngineDefaultStatusV1::RequiredWithoutDefault,
3353 ),
3354 EngineSettingDescriptorV1::new(
3355 EngineSettingIdV1::BakeAxisConversion,
3356 EngineSettingScopeV1::Document,
3357 EngineSettingDomainV1::Boolean,
3358 EngineSettingApplicabilityV1::Applicable,
3359 EngineDefaultStatusV1::RequiredWithoutDefault,
3360 ),
3361 ];
3362 let source = EnginePrimarySourceV1::new(
3363 "source",
3364 "1",
3365 "https://example.invalid/source",
3366 "2026-08-20",
3367 vec![
3368 EngineFactIdV1::AcceptedInputs,
3369 EngineFactIdV1::UnitConversionControl,
3370 EngineFactIdV1::AxisConversionControl,
3371 ],
3372 vec![
3373 EngineSettingIdV1::ConvertUnits,
3374 EngineSettingIdV1::BakeAxisConversion,
3375 ],
3376 )
3377 .unwrap();
3378 ResolvedEngineProfileV1::new(
3379 EngineProfileSelectionV1::new(family, 1, "1", "importer").unwrap(),
3380 format!("urn:animsmith:engine-profile:{family}:1"),
3381 facts,
3382 descriptors,
3383 vec![source],
3384 )
3385 .unwrap()
3386 }
3387
3388 fn document_settings() -> Vec<EngineSettingRowV1> {
3389 vec![
3390 EngineSettingRowV1::new(
3391 EngineSettingIdV1::ConvertUnits,
3392 EngineSettingValueV1::Boolean(true),
3393 ),
3394 EngineSettingRowV1::new(
3395 EngineSettingIdV1::BakeAxisConversion,
3396 EngineSettingValueV1::Boolean(false),
3397 ),
3398 ]
3399 }
3400
3401 #[test]
3402 fn profile_encoder_preserves_464_godot_golden() {
3403 let profile = godot_profile();
3404 assert_eq!(
3405 profile.facts_identity().sha256(),
3406 "e9c8316d1655c487b60dd35bbfc70289952c5fa12f4718f0be09c7e9a00fbe87"
3407 );
3408 assert_eq!(profile.facts_identity().bytes(), 1_166);
3409
3410 let mut encoder = CanonicalEncoder::default();
3411 profile.encode_preimage(&mut encoder);
3412 assert_eq!(encoder.into_bytes().len(), 1_166);
3413 }
3414
3415 #[test]
3416 fn settings_encoder_preserves_464_godot_golden() {
3417 let profile = godot_profile();
3418 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
3419 assert_eq!(
3420 settings.settings_identity().sha256(),
3421 "02032c315fa41ad65249efe1b6914456b3b98caf9b5374b168854cd357f85515"
3422 );
3423 assert_eq!(settings.settings_identity().bytes(), 240);
3424 }
3425
3426 #[test]
3427 fn constructors_canonicalize_sets_maps_and_retain_repeated_clips() {
3428 let profile = settings_profile("test");
3429 let first = ResolvedEngineSettingsV1::new(
3430 &profile,
3431 document_settings(),
3432 vec![
3433 EngineClipSettingsV1::new("walk", vec![]).unwrap(),
3434 EngineClipSettingsV1::new("idle", vec![]).unwrap(),
3435 EngineClipSettingsV1::new("walk", vec![]).unwrap(),
3436 ],
3437 )
3438 .unwrap();
3439 let mut reversed = document_settings();
3440 reversed.reverse();
3441 let second = ResolvedEngineSettingsV1::new(
3442 &profile,
3443 reversed,
3444 vec![
3445 EngineClipSettingsV1::new("walk", vec![]).unwrap(),
3446 EngineClipSettingsV1::new("walk", vec![]).unwrap(),
3447 EngineClipSettingsV1::new("idle", vec![]).unwrap(),
3448 ],
3449 )
3450 .unwrap();
3451
3452 assert_eq!(first, second);
3453 assert_eq!(
3454 first
3455 .clips()
3456 .iter()
3457 .map(EngineClipSettingsV1::clip_name)
3458 .collect::<Vec<_>>(),
3459 vec!["idle", "walk", "walk"]
3460 );
3461 assert!(first.clip_row(1, "walk").is_some());
3462 assert!(first.clip_row(1, "idle").is_none());
3463
3464 let deduplicated = ResolvedEngineSettingsV1::new(
3465 &profile,
3466 document_settings(),
3467 vec![
3468 EngineClipSettingsV1::new("idle", vec![]).unwrap(),
3469 EngineClipSettingsV1::new("walk", vec![]).unwrap(),
3470 ],
3471 )
3472 .unwrap();
3473 assert_ne!(first.settings_identity(), deduplicated.settings_identity());
3474 }
3475
3476 #[test]
3477 fn wire_round_trip_is_strict_and_revalidates_identities() {
3478 let profile = godot_profile();
3479 let value = serde_json::to_value(&profile).unwrap();
3480 assert_eq!(
3481 value["schema"],
3482 json!("urn:animsmith:engine-profile-facts:1")
3483 );
3484 assert!(value.get("primary_sources").is_some());
3485 let decoded: ResolvedEngineProfileV1 = serde_json::from_value(value.clone()).unwrap();
3486 assert_eq!(decoded, profile);
3487
3488 let mut unknown = value.clone();
3489 unknown["unexpected"] = json!(true);
3490 assert!(
3491 serde_json::from_value::<ResolvedEngineProfileV1>(unknown)
3492 .unwrap_err()
3493 .to_string()
3494 .contains("unknown field")
3495 );
3496
3497 let mut identity = value.clone();
3498 identity["identity"]["bytes"] = json!(0);
3499 assert!(
3500 serde_json::from_value::<ResolvedEngineProfileV1>(identity)
3501 .unwrap_err()
3502 .to_string()
3503 .contains("identity does not match")
3504 );
3505
3506 let mut reordered = value;
3507 reordered["facts"].as_array_mut().unwrap().swap(0, 1);
3508 assert!(
3509 serde_json::from_value::<ResolvedEngineProfileV1>(reordered)
3510 .unwrap_err()
3511 .to_string()
3512 .contains("canonical order")
3513 );
3514 }
3515
3516 #[test]
3517 fn profile_mutations_return_the_specific_first_contract_error() {
3518 let profile = godot_profile();
3519
3520 let mut changed = profile.clone();
3521 changed.schema = "urn:changed".into();
3522 assert!(matches!(
3523 changed.validate(),
3524 Err(EngineContractError::InvalidSchema {
3525 field: "profile.schema",
3526 ..
3527 })
3528 ));
3529
3530 let mut changed = profile.clone();
3531 changed.selection.family.push_str("-changed");
3532 assert_eq!(
3533 changed.validate(),
3534 Err(EngineContractError::IdentityMismatch {
3535 contract: ENGINE_PROFILE_FACTS_V1_ID,
3536 })
3537 );
3538
3539 let mut changed = profile.clone();
3540 changed.facts[0].state = EngineFactStateV1::Unknown;
3541 assert_eq!(
3542 changed.validate(),
3543 Err(EngineContractError::InvalidAcceptedInputs)
3544 );
3545
3546 let mut changed = profile;
3547 changed.primary_sources[0].url.push_str("/changed");
3548 assert_eq!(
3549 changed.validate(),
3550 Err(EngineContractError::IdentityMismatch {
3551 contract: ENGINE_PROFILE_FACTS_V1_ID,
3552 })
3553 );
3554 }
3555
3556 #[test]
3557 fn profile_acceptance_mutation_matrix_pins_tuple_facts_descriptors_and_sources() {
3558 let profile = settings_profile("matrix");
3559 let identity_mismatch = Err(EngineContractError::IdentityMismatch {
3560 contract: ENGINE_PROFILE_FACTS_V1_ID,
3561 });
3562
3563 let mut changed = profile.clone();
3564 changed.selection.family.push_str("-changed");
3565 assert_eq!(changed.validate(), identity_mismatch);
3566
3567 let mut changed = profile.clone();
3568 changed.selection.profile_revision += 1;
3569 assert_eq!(changed.validate(), identity_mismatch);
3570
3571 let mut changed = profile.clone();
3572 changed.selection.engine_version.push_str("-changed");
3573 assert_eq!(changed.validate(), identity_mismatch);
3574
3575 let mut changed = profile.clone();
3576 changed.selection.importer.push_str("-changed");
3577 assert_eq!(changed.validate(), identity_mismatch);
3578
3579 let mut changed = profile.clone();
3580 changed.fact_bundle_urn.push_str(":changed");
3581 assert_eq!(changed.validate(), identity_mismatch);
3582
3583 let mut changed = profile.clone();
3584 changed.facts.pop();
3585 assert_eq!(
3586 changed.validate(),
3587 Err(EngineContractError::InvalidFactInventory)
3588 );
3589
3590 let mut changed = profile.clone();
3591 changed
3592 .facts
3593 .iter_mut()
3594 .find(|fact| fact.id == EngineFactIdV1::AcceptedInputs)
3595 .unwrap()
3596 .state = EngineFactStateV1::Known(EngineFactValueV1::Boolean(true));
3597 assert_eq!(
3598 changed.validate(),
3599 Err(EngineContractError::InvalidFactValue {
3600 fact: EngineFactIdV1::AcceptedInputs,
3601 })
3602 );
3603
3604 let mut changed = profile.clone();
3605 changed.setting_descriptors[1].id = EngineSettingIdV1::BakeAxisConversion;
3606 assert_eq!(
3607 changed.validate(),
3608 Err(EngineContractError::InvalidFactValue {
3609 fact: EngineFactIdV1::UnitConversionControl,
3610 })
3611 );
3612
3613 let mut changed = profile.clone();
3614 changed.setting_descriptors[0].scope = EngineSettingScopeV1::Clip;
3615 assert_eq!(changed.validate(), identity_mismatch);
3616
3617 let mut changed = profile.clone();
3618 changed.setting_descriptors[0].domain = EngineSettingDomainV1::BakeOrExtract;
3619 assert_eq!(changed.validate(), identity_mismatch);
3620
3621 let mut changed = profile.clone();
3622 let descriptor_id = changed.setting_descriptors[0].id;
3623 changed.setting_descriptors[0].applicability = EngineSettingApplicabilityV1::NotApplicable;
3624 assert_eq!(
3625 changed.validate(),
3626 Err(EngineContractError::InvalidDescriptorDefault {
3627 setting: descriptor_id,
3628 })
3629 );
3630
3631 let mut changed = profile.clone();
3632 let descriptor_id = changed.setting_descriptors[0].id;
3633 changed.setting_descriptors[0].default_status = EngineDefaultStatusV1::NotApplicable;
3634 assert_eq!(
3635 changed.validate(),
3636 Err(EngineContractError::InvalidDescriptorDefault {
3637 setting: descriptor_id,
3638 })
3639 );
3640
3641 let mut changed = profile.clone();
3642 changed.primary_sources[0].id.clear();
3643 assert_eq!(
3644 changed.validate(),
3645 Err(EngineContractError::EmptyText {
3646 field: "primary_sources.id",
3647 })
3648 );
3649
3650 let mut changed = profile.clone();
3651 changed.primary_sources[0].url.push_str("/changed");
3652 assert_eq!(changed.validate(), identity_mismatch);
3653
3654 let mut changed = profile.clone();
3655 changed.primary_sources[0]
3656 .supported_fact_ids
3657 .push(EngineFactIdV1::AnimationAddressability);
3658 changed.primary_sources[0]
3659 .supported_fact_ids
3660 .sort_by_key(|id| id.as_str());
3661 assert_eq!(
3662 changed.validate(),
3663 Err(EngineContractError::SourceReferencesNonKnownFact {
3664 source_id: "source".to_owned(),
3665 fact: EngineFactIdV1::AnimationAddressability,
3666 })
3667 );
3668
3669 let mut changed = profile.clone();
3670 changed.schema = "urn:changed".to_owned();
3671 assert_eq!(
3672 changed.validate(),
3673 Err(EngineContractError::InvalidSchema {
3674 field: "profile.schema",
3675 expected: ENGINE_PROFILE_FACTS_V1_ID,
3676 found: "urn:changed".to_owned(),
3677 })
3678 );
3679
3680 let mut changed = profile;
3681 changed.identity = InputIdentity::from_bytes(b"changed");
3682 assert_eq!(changed.validate(), identity_mismatch);
3683 }
3684
3685 #[test]
3686 fn settings_wire_requires_profile_validation_after_structural_read() {
3687 let profile = settings_profile("wire");
3688 let settings =
3689 ResolvedEngineSettingsV1::new(&profile, document_settings(), vec![]).unwrap();
3690 let mut value = serde_json::to_value(&settings).unwrap();
3691 let decoded: ResolvedEngineSettingsV1 = serde_json::from_value(value.clone()).unwrap();
3692 decoded.validate_against(&profile).unwrap();
3693
3694 value["identity"]["bytes"] = json!(0);
3695 let decoded: ResolvedEngineSettingsV1 = serde_json::from_value(value).unwrap();
3696 assert_eq!(
3697 decoded.validate_against(&profile),
3698 Err(EngineContractError::IdentityMismatch {
3699 contract: RESOLVED_ENGINE_SETTINGS_V1_ID,
3700 })
3701 );
3702 }
3703
3704 #[test]
3705 fn settings_mutations_reject_noncanonical_order_before_identity() {
3706 let profile = settings_profile("order");
3707 let mut settings =
3708 ResolvedEngineSettingsV1::new(&profile, document_settings(), vec![]).unwrap();
3709 settings.document_settings.swap(0, 1);
3710 assert_eq!(
3711 settings.validate_against(&profile),
3712 Err(EngineContractError::NonCanonicalOrder {
3713 field: "settings.document_settings",
3714 })
3715 );
3716 }
3717
3718 #[test]
3719 fn materialized_settings_acceptance_mutation_matrix_pins_id_value_location_and_identity() {
3720 let profile = settings_profile("settings-matrix");
3721 let settings =
3722 ResolvedEngineSettingsV1::new(&profile, document_settings(), vec![]).unwrap();
3723
3724 let mut changed = settings.clone();
3725 changed.document_settings[1].id = EngineSettingIdV1::RootMotionSource;
3726 assert_eq!(
3727 changed.validate_against(&profile),
3728 Err(EngineContractError::UnknownMaterializedSetting {
3729 location: "document".to_owned(),
3730 setting: EngineSettingIdV1::RootMotionSource,
3731 })
3732 );
3733
3734 let mut changed = settings.clone();
3735 changed.document_settings[0].value =
3736 EngineSettingValueV1::BakeOrExtract(EngineBakeOrExtractV1::Bake);
3737 assert_eq!(
3738 changed.validate_against(&profile),
3739 Err(EngineContractError::WrongSettingDomain {
3740 location: "document".to_owned(),
3741 setting: EngineSettingIdV1::BakeAxisConversion,
3742 })
3743 );
3744
3745 let mut changed = settings.clone();
3746 changed.clips.push(
3747 EngineClipSettingsV1::new(
3748 "walk",
3749 vec![EngineSettingRowV1::new(
3750 EngineSettingIdV1::ConvertUnits,
3751 EngineSettingValueV1::Boolean(true),
3752 )],
3753 )
3754 .unwrap(),
3755 );
3756 assert_eq!(
3757 changed.validate_against(&profile),
3758 Err(EngineContractError::WrongSettingScope {
3759 location: "clip[0]".to_owned(),
3760 setting: EngineSettingIdV1::ConvertUnits,
3761 })
3762 );
3763
3764 let mut changed = settings.clone();
3765 changed.document_settings.swap(0, 1);
3766 assert_eq!(
3767 changed.validate_against(&profile),
3768 Err(EngineContractError::NonCanonicalOrder {
3769 field: "settings.document_settings",
3770 })
3771 );
3772
3773 let mut changed = settings.clone();
3774 changed.schema = "urn:changed".to_owned();
3775 assert_eq!(
3776 changed.validate_against(&profile),
3777 Err(EngineContractError::InvalidSchema {
3778 field: "settings.schema",
3779 expected: RESOLVED_ENGINE_SETTINGS_V1_ID,
3780 found: "urn:changed".to_owned(),
3781 })
3782 );
3783
3784 let mut changed = settings;
3785 changed.identity = InputIdentity::from_bytes(b"changed");
3786 assert_eq!(
3787 changed.validate_against(&profile),
3788 Err(EngineContractError::IdentityMismatch {
3789 contract: RESOLVED_ENGINE_SETTINGS_V1_ID,
3790 })
3791 );
3792 }
3793
3794 #[test]
3795 fn clip_collection_bound_accepts_exact_n_and_rejects_n_plus_one() {
3796 let profile = godot_profile();
3797 let clips = (0..ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
3798 .map(|_| EngineClipSettingsV1::new("same", vec![]).unwrap())
3799 .collect();
3800 let exact = ResolvedEngineSettingsV1::new(&profile, vec![], clips).unwrap();
3801 assert_eq!(exact.clips().len(), ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS);
3802
3803 let clips = (0..=ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS)
3804 .map(|_| EngineClipSettingsV1::new("same", vec![]).unwrap())
3805 .collect();
3806 assert_eq!(
3807 ResolvedEngineSettingsV1::new(&profile, vec![], clips),
3808 Err(EngineContractError::TooManyRows {
3809 field: "settings.clips",
3810 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
3811 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
3812 })
3813 );
3814 }
3815
3816 #[test]
3817 fn text_bound_accepts_exact_n_and_rejects_n_plus_one() {
3818 let exact = "a".repeat(ENGINE_CONTRACT_V1_MAX_TEXT_BYTES);
3819 EngineClipSettingsV1::new(
3820 "clip",
3821 vec![EngineSettingRowV1::new(
3822 EngineSettingIdV1::RootMotionSource,
3823 EngineSettingValueV1::SourceTransformPath(exact),
3824 )],
3825 )
3826 .unwrap();
3827
3828 let oversized = "a".repeat(ENGINE_CONTRACT_V1_MAX_TEXT_BYTES + 1);
3829 assert_eq!(
3830 EngineClipSettingsV1::new(
3831 "clip",
3832 vec![EngineSettingRowV1::new(
3833 EngineSettingIdV1::RootMotionSource,
3834 EngineSettingValueV1::SourceTransformPath(oversized),
3835 )],
3836 ),
3837 Err(EngineContractError::TextTooLong {
3838 field: "source_transform_path",
3839 found: ENGINE_CONTRACT_V1_MAX_TEXT_BYTES + 1,
3840 max: ENGINE_CONTRACT_V1_MAX_TEXT_BYTES,
3841 })
3842 );
3843 }
3844
3845 #[test]
3846 fn materialized_setting_mutations_name_the_violated_contract() {
3847 let profile = settings_profile("mutations");
3848 assert_eq!(
3849 ResolvedEngineSettingsV1::new(
3850 &profile,
3851 vec![EngineSettingRowV1::new(
3852 EngineSettingIdV1::ConvertUnits,
3853 EngineSettingValueV1::Boolean(true),
3854 )],
3855 vec![],
3856 ),
3857 Err(EngineContractError::MissingRequiredSetting {
3858 location: "document".into(),
3859 setting: EngineSettingIdV1::BakeAxisConversion,
3860 })
3861 );
3862 assert_eq!(
3863 ResolvedEngineSettingsV1::new(
3864 &profile,
3865 vec![
3866 EngineSettingRowV1::new(
3867 EngineSettingIdV1::ConvertUnits,
3868 EngineSettingValueV1::BakeOrExtract(EngineBakeOrExtractV1::Bake),
3869 ),
3870 EngineSettingRowV1::new(
3871 EngineSettingIdV1::BakeAxisConversion,
3872 EngineSettingValueV1::Boolean(true),
3873 ),
3874 ],
3875 vec![],
3876 ),
3877 Err(EngineContractError::WrongSettingDomain {
3878 location: "document".into(),
3879 setting: EngineSettingIdV1::ConvertUnits,
3880 })
3881 );
3882 }
3883
3884 #[test]
3885 fn source_format_and_input_identity_deserialization_are_closed() {
3886 assert_eq!(
3887 serde_json::from_str::<SourceFormatV1>("\"glb\"").unwrap(),
3888 SourceFormatV1::Glb
3889 );
3890 assert!(serde_json::from_str::<SourceFormatV1>("\"obj\"").is_err());
3891
3892 let identity = InputIdentity::from_bytes(b"identity");
3893 let wire = serde_json::to_string(&identity).unwrap();
3894 assert_eq!(
3895 serde_json::from_str::<InputIdentity>(&wire).unwrap(),
3896 identity
3897 );
3898 let mut upper = serde_json::to_value(&identity).unwrap();
3899 upper["sha256"] = json!("A".repeat(64));
3900 assert!(serde_json::from_value::<InputIdentity>(upper).is_err());
3901 }
3902
3903 #[test]
3904 fn canonical_encoder_uses_length_prefixed_tokens() {
3905 let mut encoder = CanonicalEncoder::new("domain");
3906 encoder.field("field");
3907 encoder.count(12);
3908 encode_input_identity(&mut encoder, &InputIdentity::from_bytes(b"x"));
3909 let bytes = encoder.into_bytes();
3910 assert_eq!(&bytes[..8], &6_u64.to_be_bytes());
3911 assert_eq!(&bytes[8..14], b"domain");
3912 }
3913
3914 fn assert_profile_limit(value: &serde_json::Value, expected: EngineContractError) {
3915 match decode_resolved_engine_profile_v1(&serde_json::to_string(value).unwrap()) {
3916 Err(EngineContractDecodeError::Semantic(error)) => assert_eq!(error, expected),
3917 other => panic!("expected typed profile limit, got {other:?}"),
3918 }
3919 }
3920
3921 fn assert_settings_limit(value: &serde_json::Value, expected: EngineContractError) {
3922 match decode_resolved_engine_settings_v1_with_provenance_limit(
3923 &serde_json::to_string(value).unwrap(),
3924 ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
3925 ) {
3926 Err(EngineSettingsLimitedDecodeError::Contract(
3927 EngineContractDecodeError::Semantic(error),
3928 )) => assert_eq!(error, expected),
3929 _ => panic!("expected typed settings limit"),
3930 }
3931 }
3932
3933 #[test]
3934 fn profile_sequences_reject_n_plus_one_before_decoding_null_sentinels() {
3935 let profile = settings_profile("stream-profile");
3936 let base = serde_json::to_value(&profile).unwrap();
3937 for (field, element) in [
3938 ("facts", base["facts"][0].clone()),
3939 (
3940 "setting_descriptors",
3941 base["setting_descriptors"][0].clone(),
3942 ),
3943 ("primary_sources", base["primary_sources"][0].clone()),
3944 ] {
3945 let mut rows = vec![element; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
3946 rows.push(serde_json::Value::Null);
3947 let mut over = base.clone();
3948 over[field] = rows.into();
3949 assert_profile_limit(
3950 &over,
3951 EngineContractError::TooManyRows {
3952 field: match field {
3953 "facts" => "profile.facts",
3954 "setting_descriptors" => "profile.setting_descriptors",
3955 "primary_sources" => "profile.primary_sources",
3956 _ => unreachable!(),
3957 },
3958 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
3959 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
3960 },
3961 );
3962 }
3963
3964 let mut accepted = vec![serde_json::json!("glb"); ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
3965 accepted.push(serde_json::Value::Null);
3966 let mut over = base.clone();
3967 over["facts"][0]["state"]["known"]["accepted_formats"] = accepted.into();
3968 assert_profile_limit(&over, EngineContractError::InvalidAcceptedInputs);
3969
3970 for (field, value) in [
3971 ("supported_fact_ids", serde_json::json!("accepted_inputs")),
3972 ("supported_setting_ids", serde_json::json!("convert_units")),
3973 ] {
3974 let mut rows = vec![value; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
3975 rows.push(serde_json::Value::Null);
3976 let mut over = base.clone();
3977 over["primary_sources"][0][field] = rows.into();
3978 assert_profile_limit(
3979 &over,
3980 EngineContractError::TooManyRows {
3981 field: if field == "supported_fact_ids" {
3982 "primary_sources.supported_fact_ids"
3983 } else {
3984 "primary_sources.supported_setting_ids"
3985 },
3986 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
3987 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
3988 },
3989 );
3990 }
3991 }
3992
3993 #[test]
3994 fn settings_sequences_reject_n_plus_one_before_decoding_null_sentinels() {
3995 let profile = godot_profile();
3996 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
3997 let base = serde_json::to_value(settings).unwrap();
3998 let row = serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
3999 let clip = serde_json::json!({"clip_name": "clip", "settings": []});
4000
4001 for (field, element, error_field) in [
4002 (
4003 "document_settings",
4004 row.clone(),
4005 "settings.document_settings",
4006 ),
4007 ("clips", clip.clone(), "settings.clips"),
4008 ] {
4009 let mut rows = vec![element; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
4010 rows.push(serde_json::Value::Null);
4011 let mut over = base.clone();
4012 over[field] = rows.into();
4013 assert_settings_limit(
4014 &over,
4015 EngineContractError::TooManyRows {
4016 field: error_field,
4017 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
4018 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
4019 },
4020 );
4021 }
4022
4023 let mut rows = vec![row; ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
4024 rows.push(serde_json::Value::Null);
4025 let mut over = base;
4026 over["clips"] = serde_json::json!([{"clip_name": "clip", "settings": rows}]);
4027 assert_settings_limit(
4028 &over,
4029 EngineContractError::TooManyRows {
4030 field: "settings.clips.settings",
4031 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
4032 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
4033 },
4034 );
4035 }
4036
4037 #[test]
4038 fn nested_profile_and_settings_aggregate_budgets_stop_at_global_n_plus_one() {
4039 let profile = settings_profile("aggregate-profile");
4040 let mut profile_wire = serde_json::to_value(profile).unwrap();
4041 profile_wire["facts"] = serde_json::json!([]);
4042 profile_wire["setting_descriptors"] = serde_json::json!([]);
4043 let source_template = serde_json::json!({
4044 "id": "source",
4045 "target_version": "1",
4046 "url": "https://example.invalid",
4047 "verified_on": "2026-08-20",
4048 "supported_fact_ids": vec![
4049 "accepted_inputs";
4050 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
4051 ],
4052 "supported_setting_ids": vec![
4053 "convert_units";
4054 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
4055 ]
4056 });
4057 let mut sources = vec![source_template.clone(); 7];
4058 let mut last = source_template;
4059 last["supported_setting_ids"] = serde_json::json!(vec!["convert_units"; 4_088]);
4060 last["supported_setting_ids"]
4061 .as_array_mut()
4062 .unwrap()
4063 .push(serde_json::Value::Null);
4064 sources.push(last);
4065 profile_wire["primary_sources"] = sources.into();
4066 assert_profile_limit(
4067 &profile_wire,
4068 EngineContractError::TooManyAggregateRows {
4069 found: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS + 1,
4070 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
4071 },
4072 );
4073 let mut locally_oversized =
4074 vec![serde_json::json!("convert_units"); ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS];
4075 locally_oversized.push(serde_json::Value::Null);
4076 profile_wire["primary_sources"][7]["supported_setting_ids"] = locally_oversized.into();
4077 assert_profile_limit(
4078 &profile_wire,
4079 EngineContractError::TooManyRows {
4080 field: "primary_sources.supported_setting_ids",
4081 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
4082 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
4083 },
4084 );
4085
4086 let profile = godot_profile();
4087 let settings = ResolvedEngineSettingsV1::new(&profile, vec![], vec![]).unwrap();
4088 let mut settings_wire = serde_json::to_value(settings).unwrap();
4089 let setting = serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
4090 let full_clip = serde_json::json!({
4091 "clip_name": "clip",
4092 "settings": vec![setting.clone(); 4_095]
4093 });
4094 let mut clips = vec![full_clip; 15];
4095 let mut last = serde_json::json!({
4096 "clip_name": "clip",
4097 "settings": vec![setting; 4_095]
4098 });
4099 last["settings"]
4100 .as_array_mut()
4101 .unwrap()
4102 .push(serde_json::Value::Null);
4103 clips.push(last);
4104 settings_wire["clips"] = clips.into();
4105 assert_settings_limit(
4106 &settings_wire,
4107 EngineContractError::TooManyAggregateRows {
4108 found: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS + 1,
4109 max: ENGINE_CONTRACT_V1_MAX_AGGREGATE_ROWS,
4110 },
4111 );
4112 let mut locally_oversized = vec![
4113 serde_json::json!({"id": "convert_units", "value": {"boolean": true}});
4114 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS
4115 ];
4116 locally_oversized.push(serde_json::Value::Null);
4117 settings_wire["clips"][15]["settings"] = locally_oversized.into();
4118 assert_settings_limit(
4119 &settings_wire,
4120 EngineContractError::TooManyRows {
4121 field: "settings.clips.settings",
4122 found: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS + 1,
4123 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
4124 },
4125 );
4126 }
4127}