1use serde::Serialize;
2
3use crate::domain_event::{DomainEventBodyContract, DomainEventContract};
4use crate::projection::lower::{ProjectionBodyMetadata, ProjectionPortableType};
5use crate::{
6 DomainEvent, DomainEventBodyKind, DomainEventDescriptor, DomainState, ProjectionEnvelopeField,
7 ProjectionEventSelector, ProjectionValue,
8};
9
10#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
16#[serde(tag = "kind", rename_all = "snake_case")]
17pub enum CommandProjectionPreviewSource {
18 InputPath { path: Vec<String> },
20 GeneratedDefaultPath { path: Vec<String> },
22 TrustedPreset { name: String, codec: String },
24 Constant { value: ProjectionValue },
26 Null,
28 Absent,
30 Unknown,
32 ServerOnly,
37}
38
39impl CommandProjectionPreviewSource {
40 pub fn input(path: impl IntoIterator<Item = impl Into<String>>) -> Self {
42 Self::InputPath {
43 path: path.into_iter().map(Into::into).collect(),
44 }
45 }
46
47 pub fn generated_default(path: impl IntoIterator<Item = impl Into<String>>) -> Self {
49 Self::GeneratedDefaultPath {
50 path: path.into_iter().map(Into::into).collect(),
51 }
52 }
53
54 pub fn trusted(name: impl Into<String>, codec: impl Into<String>) -> Self {
56 Self::TrustedPreset {
57 name: name.into(),
58 codec: codec.into(),
59 }
60 }
61
62 pub fn constant(value: ProjectionValue) -> Self {
64 Self::Constant { value }
65 }
66}
67
68#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
70pub struct CommandProjectionPreviewField {
71 pub(crate) body_path: Vec<String>,
72 pub(crate) envelope: Option<ProjectionEnvelopeField>,
73 #[serde(skip)]
74 pub(crate) body_type: Option<ProjectionPortableType>,
75 #[serde(skip)]
76 pub(crate) body_rust_type: Option<&'static str>,
77 #[serde(skip)]
78 pub(crate) body_nullable: Option<bool>,
79 #[serde(skip)]
80 pub(crate) body_always_present: Option<bool>,
81 pub(crate) source: CommandProjectionPreviewSource,
82}
83
84#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
90pub struct CommandProjectionPreview {
91 pub(crate) selectors: Vec<ProjectionEventSelector>,
92 pub(crate) declaration_errors: Vec<String>,
93 pub(crate) fields: Vec<CommandProjectionPreviewField>,
94}
95
96impl CommandProjectionPreview {
97 pub fn new() -> Self {
99 Self::default()
100 }
101
102 #[must_use]
105 pub fn events(mut self, events: CommandProjectionEventSet) -> Self {
106 self.selectors = events.selectors;
107 self.declaration_errors = events.declaration_errors;
108 self
109 }
110
111 #[must_use]
113 pub fn field(
114 mut self,
115 body_path: impl IntoIterator<Item = impl Into<String>>,
116 source: CommandProjectionPreviewSource,
117 ) -> Self {
118 self.fields.push(CommandProjectionPreviewField {
119 body_path: body_path.into_iter().map(Into::into).collect(),
120 envelope: None,
121 body_type: None,
122 body_rust_type: None,
123 body_nullable: None,
124 body_always_present: None,
125 source,
126 });
127 self
128 }
129
130 #[must_use]
132 pub fn envelope(
133 mut self,
134 field: ProjectionEnvelopeField,
135 source: CommandProjectionPreviewSource,
136 ) -> Self {
137 self.fields.push(CommandProjectionPreviewField {
138 body_path: Vec::new(),
139 envelope: Some(field),
140 body_type: None,
141 body_rust_type: None,
142 body_nullable: None,
143 body_always_present: None,
144 source,
145 });
146 self
147 }
148}
149
150#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
152pub(crate) struct CommandProjectionEventPreview {
153 pub selector: ProjectionEventSelector,
154 pub preview: CommandProjectionPreview,
155}
156
157#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
165pub struct CommandProjectionPureReduce {
166 pub fn_name: String,
168 pub client_module: String,
170 pub client_export: String,
172 pub wasm_package: String,
174 pub wasm_export: String,
176 pub model: String,
178 pub key: Vec<CommandProjectionPureArg>,
180 pub args: Vec<CommandProjectionPureArg>,
182 pub assign: Vec<String>,
184}
185
186#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
188pub struct CommandProjectionPureArg {
189 pub name: String,
190 pub source: CommandProjectionPreviewSource,
191}
192
193impl CommandProjectionPureReduce {
194 pub fn client_module(
196 fn_name: impl Into<String>,
197 client_module: impl Into<String>,
198 client_export: impl Into<String>,
199 model: impl Into<String>,
200 ) -> Self {
201 Self {
202 fn_name: fn_name.into(),
203 client_module: client_module.into(),
204 client_export: client_export.into(),
205 wasm_package: String::new(),
206 wasm_export: String::new(),
207 model: model.into(),
208 key: Vec::new(),
209 args: Vec::new(),
210 assign: Vec::new(),
211 }
212 }
213
214 pub fn wasm(
216 fn_name: impl Into<String>,
217 wasm_package: impl Into<String>,
218 wasm_export: impl Into<String>,
219 model: impl Into<String>,
220 ) -> Self {
221 Self {
222 fn_name: fn_name.into(),
223 client_module: String::new(),
224 client_export: String::new(),
225 wasm_package: wasm_package.into(),
226 wasm_export: wasm_export.into(),
227 model: model.into(),
228 key: Vec::new(),
229 args: Vec::new(),
230 assign: Vec::new(),
231 }
232 }
233
234 #[deprecated(note = "use client_module() or wasm()")]
236 pub fn new(
237 fn_name: impl Into<String>,
238 client_module: impl Into<String>,
239 client_export: impl Into<String>,
240 model: impl Into<String>,
241 ) -> Self {
242 Self::client_module(fn_name, client_module, client_export, model)
243 }
244
245 #[must_use]
246 pub fn key_input(
247 mut self,
248 field: impl Into<String>,
249 path: impl IntoIterator<Item = impl Into<String>>,
250 ) -> Self {
251 self.key.push(CommandProjectionPureArg {
252 name: field.into(),
253 source: CommandProjectionPreviewSource::input(path),
254 });
255 self
256 }
257
258 #[must_use]
259 pub fn arg_input(
260 mut self,
261 name: impl Into<String>,
262 path: impl IntoIterator<Item = impl Into<String>>,
263 ) -> Self {
264 self.args.push(CommandProjectionPureArg {
265 name: name.into(),
266 source: CommandProjectionPreviewSource::input(path),
267 });
268 self
269 }
270
271 #[must_use]
272 pub fn assign(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
273 self.assign.extend(fields.into_iter().map(Into::into));
274 self.assign.sort();
275 self.assign.dedup();
276 self
277 }
278}
279
280#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
282pub(crate) struct CommandProjectionEvents {
283 pub selectors: Vec<ProjectionEventSelector>,
284 pub previews: Vec<CommandProjectionEventPreview>,
285 #[serde(skip)]
288 pub inferred_values: Vec<CommandProjectionEventPreview>,
289 pub pure_reduces: Vec<CommandProjectionPureReduce>,
291 pub declaration_errors: Vec<String>,
292}
293
294impl CommandProjectionEvents {
295 pub(crate) fn add_event_set(&mut self, events: CommandProjectionEventSet) {
296 self.selectors.extend(events.selectors);
297 self.declaration_errors.extend(events.declaration_errors);
298 }
299
300 pub(crate) fn add_preview(&mut self, preview: CommandProjectionPreview) {
301 self.declaration_errors
302 .extend(preview.declaration_errors.clone());
303 if preview.selectors.is_empty() {
304 self.declaration_errors
305 .push("projection preview must bind exactly one emitted event variant".to_owned());
306 return;
307 }
308 if preview.selectors.len() != 1 {
309 self.declaration_errors.push(
310 "projection preview must bind one exact event variant, not a multi-event set"
311 .to_owned(),
312 );
313 return;
314 }
315 self.previews
316 .extend(preview.selectors.iter().cloned().map(|selector| {
317 CommandProjectionEventPreview {
318 selector,
319 preview: preview.clone(),
320 }
321 }));
322 }
323
324 pub(crate) fn add_inferred_values(&mut self, values: CommandProjectionPreview) {
325 self.declaration_errors
326 .extend(values.declaration_errors.clone());
327 if values.selectors.len() != 1 {
328 self.declaration_errors.push(
329 "inferred transition values must bind exactly one emitted event variant".to_owned(),
330 );
331 return;
332 }
333 let selector = values.selectors[0].clone();
334 if let Some(existing) = self
335 .inferred_values
336 .iter_mut()
337 .find(|candidate| candidate.selector == selector)
338 {
339 existing.preview.fields.extend(values.fields);
340 existing
341 .preview
342 .declaration_errors
343 .extend(values.declaration_errors);
344 } else {
345 self.inferred_values.push(CommandProjectionEventPreview {
346 selector,
347 preview: values,
348 });
349 }
350 }
351
352 pub(crate) fn add_authenticated_user_field(
353 &mut self,
354 rust_field: &str,
355 values: CommandProjectionPreview,
356 ) {
357 if values.fields.len() != 1 {
358 self.declaration_errors.push(
359 format!(
360 "authenticated-user inference field `{rust_field}` is not one exact emitted-event body field"
361 ),
362 );
363 return;
364 }
365 self.add_inferred_values(values);
366 }
367
368 pub(crate) fn add_pure_reduce(&mut self, reduce: CommandProjectionPureReduce) {
369 self.pure_reduces.push(reduce);
370 }
371
372 pub(crate) fn canonicalize_and_validate(&mut self, command: &str) -> Result<(), String> {
373 if let Some(error) = self.declaration_errors.first() {
374 return Err(format!(
375 "typed command `{command}` has an invalid domain-event declaration: {error}"
376 ));
377 }
378 self.selectors
379 .sort_by(ProjectionEventSelector::canonical_cmp);
380 if self.selectors.windows(2).any(|pair| pair[0] == pair[1]) {
381 return Err(format!(
382 "typed command `{command}` repeats an exact emitted domain event selector"
383 ));
384 }
385 for pair in self.selectors.windows(2) {
386 if pair[0].event_name() == pair[1].event_name()
387 && pair[0].event_version() == pair[1].event_version()
388 && pair[0] != pair[1]
389 {
390 return Err(format!(
391 "typed command `{command}` declares conflicting schemas for domain event `{}` v{}",
392 pair[0].event_name(),
393 pair[0].event_version()
394 ));
395 }
396 }
397 canonicalize_preview_declarations(command, &self.selectors, &mut self.previews, false)?;
402 canonicalize_preview_declarations(
403 command,
404 &self.selectors,
405 &mut self.inferred_values,
406 true,
407 )?;
408 for reduce in &mut self.pure_reduces {
409 if reduce.fn_name.trim().is_empty() || reduce.model.trim().is_empty() {
410 return Err(format!(
411 "typed command `{command}` pure reduce requires non-empty fn and model"
412 ));
413 }
414 let hand =
415 !reduce.client_module.trim().is_empty() || !reduce.client_export.trim().is_empty();
416 let wasm =
417 !reduce.wasm_package.trim().is_empty() || !reduce.wasm_export.trim().is_empty();
418 if hand == wasm {
419 return Err(format!(
420 "typed command `{command}` pure reduce `{}` must declare either client_module+client_export or wasm_package+wasm_export (not both, not neither)",
421 reduce.fn_name
422 ));
423 }
424 if hand
425 && (reduce.client_module.trim().is_empty()
426 || reduce.client_export.trim().is_empty())
427 {
428 return Err(format!(
429 "typed command `{command}` pure reduce `{}` client module requires non-empty client_module and client_export",
430 reduce.fn_name
431 ));
432 }
433 if wasm
434 && (reduce.wasm_package.trim().is_empty() || reduce.wasm_export.trim().is_empty())
435 {
436 return Err(format!(
437 "typed command `{command}` pure reduce `{}` wasm package requires non-empty wasm_package and wasm_export",
438 reduce.fn_name
439 ));
440 }
441 if reduce.key.is_empty() {
442 return Err(format!(
443 "typed command `{command}` pure reduce `{}` requires at least one key field",
444 reduce.fn_name
445 ));
446 }
447 if reduce.assign.is_empty() {
448 return Err(format!(
449 "typed command `{command}` pure reduce `{}` requires at least one assign field",
450 reduce.fn_name
451 ));
452 }
453 reduce.key.sort_by(|a, b| a.name.cmp(&b.name));
454 reduce.args.sort_by(|a, b| a.name.cmp(&b.name));
455 reduce.assign.sort();
456 reduce.assign.dedup();
457 for arg in reduce.key.iter().chain(reduce.args.iter()) {
458 match &arg.source {
459 CommandProjectionPreviewSource::InputPath { path }
460 | CommandProjectionPreviewSource::GeneratedDefaultPath { path } => {
461 validate_path(command, "pure reduce", path)?;
462 }
463 CommandProjectionPreviewSource::TrustedPreset { name, codec } => {
464 if name.trim().is_empty() || codec.trim().is_empty() {
465 return Err(format!(
466 "typed command `{command}` pure reduce trusted preset name and codec must not be empty"
467 ));
468 }
469 }
470 other => {
471 return Err(format!(
472 "typed command `{command}` pure reduce `{}` arg `{}` uses unsupported source {other:?}",
473 reduce.fn_name, arg.name
474 ));
475 }
476 }
477 }
478 }
479 self.pure_reduces
480 .sort_by(|left, right| left.fn_name.cmp(&right.fn_name));
481 if self
482 .pure_reduces
483 .windows(2)
484 .any(|pair| pair[0].fn_name == pair[1].fn_name)
485 {
486 return Err(format!(
487 "typed command `{command}` repeats pure reduce fn name"
488 ));
489 }
490 Ok(())
491 }
492}
493
494fn canonicalize_preview_declarations(
495 command: &str,
496 selectors: &[ProjectionEventSelector],
497 previews: &mut [CommandProjectionEventPreview],
498 inferred: bool,
499) -> Result<(), String> {
500 for preview in previews {
501 if selectors
502 .binary_search_by(|selector| selector.canonical_cmp(&preview.selector))
503 .is_err()
504 {
505 let source = if inferred {
506 "infers transition values"
507 } else {
508 "declares preview provenance"
509 };
510 return Err(format!(
511 "typed command `{command}` {source} outside its exact emitted event set"
512 ));
513 }
514 preview.preview.fields.sort_by_key(preview_field_key);
515 for pair in preview.preview.fields.windows(2) {
516 if preview_field_key(&pair[0]) == preview_field_key(&pair[1]) {
517 let source = if inferred {
518 "inferred transition value"
519 } else {
520 "preview provenance"
521 };
522 return Err(format!(
523 "typed command `{command}` repeats {source} for one event value"
524 ));
525 }
526 }
527 for field in &preview.preview.fields {
528 if field.envelope.is_none() {
529 validate_path(command, "emitted body", &field.body_path)?;
530 }
531 match &field.source {
532 CommandProjectionPreviewSource::InputPath { path }
533 | CommandProjectionPreviewSource::GeneratedDefaultPath { path } => {
534 validate_path(command, "preview input", path)?;
535 }
536 CommandProjectionPreviewSource::TrustedPreset { name, codec } => {
537 if name.trim().is_empty() || codec.trim().is_empty() {
538 return Err(format!(
539 "typed command `{command}` preview trusted preset name and codec must not be empty"
540 ));
541 }
542 }
543 CommandProjectionPreviewSource::ServerOnly => {
544 return Err(format!(
545 "typed command `{command}` cannot expose server-only preview provenance"
546 ));
547 }
548 CommandProjectionPreviewSource::Constant { .. }
549 | CommandProjectionPreviewSource::Null
550 | CommandProjectionPreviewSource::Absent
551 | CommandProjectionPreviewSource::Unknown => {}
552 }
553 }
554 }
555 Ok(())
556}
557
558#[derive(Clone, Debug, Default, PartialEq, Eq)]
560pub struct CommandProjectionEventSet {
561 selectors: Vec<ProjectionEventSelector>,
562 declaration_errors: Vec<String>,
563}
564
565#[doc(hidden)]
567pub fn __command_projection_events(
568 descriptors: impl IntoIterator<Item = Result<DomainEventDescriptor, String>>,
569) -> CommandProjectionEventSet {
570 let mut events = CommandProjectionEventSet::default();
571 for descriptor in descriptors {
572 let descriptor = match descriptor {
573 Ok(descriptor) => descriptor,
574 Err(error) => {
575 events.declaration_errors.push(error);
576 continue;
577 }
578 };
579 match ProjectionEventSelector::try_from_descriptor(&descriptor) {
580 Ok(selector) => events.selectors.push(selector),
581 Err(error) => events.declaration_errors.push(error.to_string()),
582 }
583 }
584 events
585}
586
587#[doc(hidden)]
589pub fn __command_projection_event_descriptor<E: DomainEventContract>(
590) -> Result<DomainEventDescriptor, String> {
591 let descriptor = E::descriptor();
592 if descriptor.name != E::EVENT_NAME {
593 return Err(format!(
594 "event contract name `{}` differs from descriptor name `{}`",
595 E::EVENT_NAME,
596 descriptor.name
597 ));
598 }
599 if descriptor.version != E::EVENT_VERSION {
600 return Err(format!(
601 "event contract `{}` version {} differs from descriptor version {}",
602 E::EVENT_NAME,
603 E::EVENT_VERSION,
604 descriptor.version
605 ));
606 }
607 Ok(descriptor)
608}
609
610#[doc(hidden)]
612pub fn __command_projection_state_preview<E, S>(
613 fields: Vec<(&'static str, CommandProjectionPreviewSource)>,
614) -> CommandProjectionPreview
615where
616 E: DomainEventBodyContract<S>,
617 S: DomainState + ProjectionBodyMetadata,
618{
619 let descriptor = __command_projection_event_descriptor::<E>().and_then(|descriptor| {
620 let expected = DomainEventDescriptor::state::<S>(E::EVENT_NAME, E::EVENT_VERSION);
621 if descriptor != expected || descriptor.body.kind != DomainEventBodyKind::State {
622 return Err(format!(
623 "state preview event contract `{}` does not exactly describe `{}` state",
624 E::EVENT_NAME,
625 std::any::type_name::<S>()
626 ));
627 }
628 Ok(descriptor)
629 });
630 structured_preview::<S>(__command_projection_events([descriptor]), fields)
631}
632
633#[doc(hidden)]
640pub fn __command_projection_state_known_values<E, S>(
641 fields: Vec<(&'static str, CommandProjectionPreviewSource)>,
642) -> CommandProjectionPreview
643where
644 E: DomainEventBodyContract<S>,
645 S: DomainState + ProjectionBodyMetadata,
646{
647 let descriptor = __command_projection_event_descriptor::<E>().and_then(|descriptor| {
648 let expected = DomainEventDescriptor::state::<S>(E::EVENT_NAME, E::EVENT_VERSION);
649 if descriptor != expected || descriptor.body.kind != DomainEventBodyKind::State {
650 return Err(format!(
651 "inferred transition event contract `{}` does not exactly describe `{}` state",
652 E::EVENT_NAME,
653 std::any::type_name::<S>()
654 ));
655 }
656 Ok(descriptor)
657 });
658 let mut values =
659 CommandProjectionPreview::new().events(__command_projection_events([descriptor]));
660 for (rust_name, source) in fields {
661 let Some(field) = S::PROJECTION_FIELDS
662 .iter()
663 .find(|field| field.rust_name == rust_name && field.present)
664 else {
665 continue;
666 };
667 values.fields.push(CommandProjectionPreviewField {
668 body_path: vec![field.wire_name.to_owned()],
669 envelope: None,
670 body_type: Some(field.portable_type),
671 body_rust_type: Some(field.rust_type),
672 body_nullable: Some(field.nullable),
673 body_always_present: Some(field.always_present),
674 source,
675 });
676 }
677 values
678}
679
680#[doc(hidden)]
682pub fn __command_projection_event_preview<E, B>(
683 fields: Vec<(&'static str, CommandProjectionPreviewSource)>,
684) -> CommandProjectionPreview
685where
686 E: DomainEventBodyContract<B>,
687 B: DomainEvent + ProjectionBodyMetadata,
688{
689 let descriptor = __command_projection_event_descriptor::<E>().and_then(|descriptor| {
690 if descriptor != B::DESCRIPTOR || descriptor.body.kind != DomainEventBodyKind::Event {
691 return Err(format!(
692 "event preview contract `{}` differs from its exact typed body descriptor",
693 E::EVENT_NAME
694 ));
695 }
696 Ok(descriptor)
697 });
698 structured_preview::<B>(__command_projection_events([descriptor]), fields)
699}
700
701fn structured_preview<B: ProjectionBodyMetadata>(
702 events: CommandProjectionEventSet,
703 fields: Vec<(&'static str, CommandProjectionPreviewSource)>,
704) -> CommandProjectionPreview {
705 let mut preview = CommandProjectionPreview::new().events(events);
706 for (rust_name, source) in fields {
707 match B::PROJECTION_FIELDS
708 .iter()
709 .find(|field| field.rust_name == rust_name && field.present)
710 {
711 Some(field) => preview.fields.push(CommandProjectionPreviewField {
712 body_path: vec![field.wire_name.to_owned()],
713 envelope: None,
714 body_type: Some(field.portable_type),
715 body_rust_type: Some(field.rust_type),
716 body_nullable: Some(field.nullable),
717 body_always_present: Some(field.always_present),
718 source,
719 }),
720 None => preview.declaration_errors.push(format!(
721 "state preview references unknown body field `{rust_name}`"
722 )),
723 }
724 }
725 preview
726}
727
728#[doc(hidden)]
730pub fn __command_projection_preview_constant(
731 value: impl Serialize,
732) -> CommandProjectionPreviewSource {
733 match serde_json::to_value(value)
734 .map_err(|error| error.to_string())
735 .and_then(|value| ProjectionValue::try_from_json(value).map_err(|error| error.to_string()))
736 {
737 Ok(value) => CommandProjectionPreviewSource::Constant { value },
738 Err(_) => CommandProjectionPreviewSource::Unknown,
739 }
740}
741
742pub trait CommandEventSet {
754 fn command_event_set() -> CommandProjectionEventSet;
756
757 fn command_event_known_values() -> Vec<CommandProjectionPreview> {
763 Vec::new()
764 }
765}
766
767impl<E: DomainEventContract> CommandEventSet for E {
768 fn command_event_set() -> CommandProjectionEventSet {
769 __command_projection_events([__command_projection_event_descriptor::<E>()])
770 }
771}
772
773macro_rules! impl_command_event_set_tuple {
774 ($($E:ident),+) => {
775 impl<$($E: DomainEventContract),+> CommandEventSet for ($($E,)+) {
776 fn command_event_set() -> CommandProjectionEventSet {
777 __command_projection_events([
778 $(__command_projection_event_descriptor::<$E>()),+
779 ])
780 }
781 }
782 };
783}
784
785impl_command_event_set_tuple!(E1, E2);
786impl_command_event_set_tuple!(E1, E2, E3);
787impl_command_event_set_tuple!(E1, E2, E3, E4);
788impl_command_event_set_tuple!(E1, E2, E3, E4, E5);
789impl_command_event_set_tuple!(E1, E2, E3, E4, E5, E6);
790impl_command_event_set_tuple!(E1, E2, E3, E4, E5, E6, E7);
791impl_command_event_set_tuple!(E1, E2, E3, E4, E5, E6, E7, E8);
792
793#[macro_export]
795macro_rules! events {
796 ($($event:ty),+ $(,)?) => {
797 $crate::graphql::__command_projection_events([
798 $($crate::graphql::__command_projection_event_descriptor::<$event>()),+
799 ])
800 };
801}
802
803#[macro_export]
808macro_rules! state_preview {
809 (
810 $event:ty => $state:ty { $($fields:tt)* }
811 ) => {{
812 $crate::graphql::__command_projection_state_preview::<$event, $state>(
813 $crate::__distributed_state_preview_fields!(@collect [] ; $($fields)*)
814 )
815 }};
816}
817
818#[macro_export]
820macro_rules! event_preview {
821 (
822 $event:ty => $body:ty { $($fields:tt)* }
823 ) => {{
824 $crate::graphql::__command_projection_event_preview::<$event, $body>(
825 $crate::__distributed_state_preview_fields!(@collect [] ; $($fields)*)
826 )
827 }};
828}
829
830#[doc(hidden)]
831#[macro_export]
832macro_rules! __distributed_state_preview_fields {
833 (@collect [$($out:expr,)*] ; ..unknown $(,)?) => {
834 vec![$($out,)*]
835 };
836 (@collect [$($out:expr,)*] ; ) => {
837 vec![$($out,)*]
838 };
839 (@collect [$($out:expr,)*] ;
840 $field:ident : input.$first:ident $(.$rest:ident)*,
841 $($tail:tt)*
842 ) => {
843 $crate::__distributed_state_preview_fields!(
844 @collect [
845 $($out,)*
846 (
847 stringify!($field),
848 $crate::graphql::CommandProjectionPreviewSource::input([
849 stringify!($first) $(, stringify!($rest))*
850 ])
851 ),
852 ];
853 $($tail)*
854 )
855 };
856 (@collect [$($out:expr,)*] ;
857 $field:ident : generated.$first:ident $(.$rest:ident)*,
858 $($tail:tt)*
859 ) => {
860 $crate::__distributed_state_preview_fields!(
861 @collect [
862 $($out,)*
863 (
864 stringify!($field),
865 $crate::graphql::CommandProjectionPreviewSource::generated_default([
866 stringify!($first) $(, stringify!($rest))*
867 ])
868 ),
869 ];
870 $($tail)*
871 )
872 };
873 (@collect [$($out:expr,)*] ;
874 $field:ident : trusted($name:expr, $codec:expr),
875 $($tail:tt)*
876 ) => {
877 $crate::__distributed_state_preview_fields!(
878 @collect [
879 $($out,)*
880 (
881 stringify!($field),
882 $crate::graphql::CommandProjectionPreviewSource::trusted($name, $codec)
883 ),
884 ];
885 $($tail)*
886 )
887 };
888 (@collect [$($out:expr,)*] ; $field:ident : unknown, $($tail:tt)*) => {
889 $crate::__distributed_state_preview_fields!(
890 @collect [$($out,)* (stringify!($field), $crate::graphql::CommandProjectionPreviewSource::Unknown),];
891 $($tail)*
892 )
893 };
894 (@collect [$($out:expr,)*] ; $field:ident : absent, $($tail:tt)*) => {
895 $crate::__distributed_state_preview_fields!(
896 @collect [$($out,)* (stringify!($field), $crate::graphql::CommandProjectionPreviewSource::Absent),];
897 $($tail)*
898 )
899 };
900 (@collect [$($out:expr,)*] ; $field:ident : null, $($tail:tt)*) => {
901 $crate::__distributed_state_preview_fields!(
902 @collect [$($out,)* (stringify!($field), $crate::graphql::CommandProjectionPreviewSource::Null),];
903 $($tail)*
904 )
905 };
906 (@collect [$($out:expr,)*] ; $field:ident : $constant:path, $($tail:tt)*) => {
907 $crate::__distributed_state_preview_fields!(
908 @collect [
909 $($out,)*
910 (
911 stringify!($field),
912 $crate::graphql::__command_projection_preview_constant($constant)
913 ),
914 ];
915 $($tail)*
916 )
917 };
918 (@collect [$($out:expr,)*] ; $field:ident : $constant:literal, $($tail:tt)*) => {
919 $crate::__distributed_state_preview_fields!(
920 @collect [
921 $($out,)*
922 (
923 stringify!($field),
924 $crate::graphql::__command_projection_preview_constant($constant)
925 ),
926 ];
927 $($tail)*
928 )
929 };
930}
931
932fn validate_path(command: &str, label: &str, path: &[String]) -> Result<(), String> {
933 if path.is_empty() || path.iter().any(|segment| segment.trim().is_empty()) {
934 return Err(format!(
935 "typed command `{command}` {label} path must contain only non-empty segments"
936 ));
937 }
938 Ok(())
939}
940
941fn preview_field_key(field: &CommandProjectionPreviewField) -> (u8, Vec<String>) {
942 match field.envelope {
943 Some(envelope) => (
944 1,
945 vec![serde_json::to_string(&envelope)
946 .expect("projection envelope field serialization cannot fail")],
947 ),
948 None => (0, field.body_path.clone()),
949 }
950}