Skip to main content

rsynko_memory/
laws.rs

1//! The context every specification law scenario runs against.
2//!
3//! One value ties every sort at once, but it states almost nothing itself: each capability is
4//! delegated to the component interpreter that already provides it. The struct is generic in those
5//! components, so the composition is structural rather than hard-wired, and with zero-sized
6//! syntax interpreters the compiler leaves no trace of it.
7
8use crate::{
9    Extraction, Format, InfoRecord, ManagerState, Media, MediaSyntax, ProcessingStep,
10    ProcessingSyntax, ProcessorId, REFERENCE_MEDIA_EXTENSION, REFERENCE_MEDIA_URL,
11    REFERENCE_VIDEO_ID, REFERENCE_WATCH_URL, ReferenceDownloadEnv, ReferenceExtractor,
12    ReferenceExtractorRegistry, ReferenceYoutubeDownloadEnv, ReferenceYoutubeEnv, YoutubeRequest,
13};
14use crate::{
15    MemoryManager, PlannedChange, QueueEntry, ReferenceSyncEnv, RsyncEndpoint, SyncCommand,
16    SyncObservation, TextScreen,
17};
18use ambassador::Delegate;
19use rsynko_download::*;
20use rsynko_manager::*;
21use rsynko_media::*;
22use rsynko_rsync::*;
23use rsynko_ui::ScreenLawFixture;
24use rsynko_yt::*;
25use std::path::{Path, PathBuf};
26
27const LAW_RESOURCE: &str = "memory://law-resource";
28const LAW_BYTES: &[u8] = b"exact bytes";
29
30/// Denotes a processor the reference context refuses to apply.
31#[derive(Debug, thiserror::Error)]
32#[error("processor {0} refused")]
33pub struct ReferenceProcessingRefusal(String);
34
35/// Composes reference interpreters into one context that satisfies every law scenario.
36// Several capabilities delegate to the same component, which Clippy reads as a repeated attribute.
37#[allow(
38    clippy::duplicated_attributes,
39    reason = "one delegation per capability, not per target"
40)]
41#[derive(Debug, Delegate)]
42#[delegate(MetadataAlg, target = "syntax")]
43#[delegate(FormatAlg, target = "syntax")]
44#[delegate(FormatViewAlg, target = "syntax")]
45#[delegate(ArtifactAlg, target = "syntax")]
46#[delegate(ExtractionAlg, target = "syntax")]
47#[delegate(ExtractionViewAlg, target = "syntax")]
48#[delegate(MediaViewAlg, target = "syntax")]
49#[delegate(FormatPredicateAlg, target = "syntax")]
50#[delegate(FormatSelectionAlg, target = "syntax")]
51#[delegate(FormatPredicateMatchAlg, target = "syntax")]
52#[delegate(FormatSelectionApplyAlg, target = "syntax")]
53#[delegate(OutputNameAlg, target = "syntax")]
54#[delegate(ProcessingProgramAlg, target = "programs")]
55#[delegate(
56    ExtractionCatalogAlg,
57    target = "catalog",
58    where = "Catalog: ExtractionCatalogAlg + MediaSorts<Extractor = <Syntax as MediaSorts>::Extractor>, Syntax: MediaSorts"
59)]
60#[delegate(
61    ExtractionApplyAlg,
62    target = "catalog",
63    where = "Catalog: ExtractionApplyAlg + MediaSorts<Extractor = <Syntax as MediaSorts>::Extractor, Extraction = <Syntax as MediaSorts>::Extraction>, Syntax: MediaSorts"
64)]
65#[delegate(DraftStateAlg, target = "manager")]
66#[delegate(OutputDraftAlg, target = "manager")]
67#[delegate(InputDraftAlg, target = "manager")]
68#[delegate(TextEditorStateAlg, target = "manager")]
69#[delegate(NavigationStateAlg, target = "manager")]
70#[delegate(DetailSelectionAlg, target = "manager")]
71#[delegate(ManagerStatusAlg, target = "manager")]
72#[delegate(SafeExitAlg, target = "manager")]
73#[delegate(QueueCatalogAlg, target = "manager")]
74#[delegate(QueueAppendAlg, target = "manager")]
75#[delegate(QueueRemoveAlg, target = "manager")]
76#[delegate(QueuePauseAlg, target = "manager")]
77#[delegate(QueueDuplicateAlg, target = "manager")]
78#[delegate(QueueFormatEditAlg, target = "manager")]
79#[delegate(QueueOutputAlg, target = "manager")]
80#[delegate(QueueSourceAlg, target = "manager")]
81#[delegate(FormatCatalogStateAlg, target = "manager")]
82#[delegate(QueueDryRunAlg, target = "manager")]
83#[delegate(RehearsalStateAlg, target = "manager")]
84#[delegate(SourceMetadataAlg, target = "manager")]
85#[delegate(DownloadLogAlg, target = "manager")]
86#[delegate(TransferStateAlg, target = "manager")]
87#[delegate(SourceRequestAlg, target = "manager")]
88#[delegate(SourceRecognitionAlg, target = "manager")]
89#[delegate(SubmissionAlg, target = "manager")]
90#[delegate(MediaOptionsAlg, target = "manager")]
91#[delegate(OutputChoiceAlg, target = "manager")]
92#[delegate(YoutubeUrlAlg, target = "youtube")]
93#[delegate(
94    YoutubeRequestAlg,
95    target = "youtube",
96    where = "Youtube: YoutubeRequestAlg + YoutubeSorts"
97)]
98#[delegate(YoutubeProgramAlg, target = "youtube")]
99#[delegate(YoutubeClientAlg, target = "youtube")]
100#[delegate(
101    YoutubeRequestBytesAlg,
102    target = "youtube",
103    where = "Youtube: YoutubeRequestBytesAlg + YoutubeSorts"
104)]
105#[delegate(YoutubeResponseAlg, target = "youtube")]
106#[delegate(
107    YoutubeChallengeAlg,
108    target = "youtube",
109    where = "Youtube: YoutubeChallengeAlg + YoutubeSorts"
110)]
111#[delegate(
112    YoutubeSolutionAlg,
113    target = "youtube",
114    where = "Youtube: YoutubeSolutionAlg + YoutubeSorts"
115)]
116#[delegate(AtomicPublishAlg, target = "downloads")]
117#[delegate(RsyncEndpointAlg, target = "sync")]
118#[delegate(RsyncEndpointViewAlg, target = "sync")]
119#[delegate(SyncCommandAlg, target = "sync")]
120#[delegate(SyncCommandViewAlg, target = "sync")]
121#[delegate(SyncChangeAlg, target = "sync")]
122#[delegate(SyncObservationAlg, target = "sync")]
123#[delegate(SyncObservationViewAlg, target = "sync")]
124#[delegate(SyncRunAlg, target = "sync")]
125#[delegate(SyncWatchAlg, target = "sync")]
126#[delegate(DownloadObservationAlg, target = "downloads")]
127#[delegate(DownloadReportAlg, target = "downloads")]
128#[delegate(DownloadProgressAlg, target = "downloads")]
129pub struct LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager> {
130    syntax: Syntax,
131    programs: Programs,
132    catalog: Catalog,
133    downloads: Downloads,
134    youtube: Youtube,
135    manager: Manager,
136    sync: ReferenceSyncEnv,
137    applied: Vec<String>,
138    refused: Option<String>,
139}
140
141impl<Syntax, Programs, Catalog, Downloads, Youtube> RehearsalLawFixture
142    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, ManagerState>
143{
144    fn law_change(&self, path: &str, kind: ChangeKind, size: Option<u64>) -> Self::Change {
145        PlannedChange::new(path.to_owned(), kind, size)
146    }
147
148    fn law_offer_rehearsal(&mut self, id: Self::Id) {
149        self.manager.transfer_request(id);
150    }
151}
152
153impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager> RsyncSorts
154    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
155{
156    type Endpoint = RsyncEndpoint;
157    type Command = SyncCommand;
158    type Observation = SyncObservation;
159    type Change = PlannedChange;
160}
161
162impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager> SubmissionLawFixture
163    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
164{
165    fn law_folder_submission(&self) -> (String, String, String) {
166        let input = "backup@nas.local:/volume1/photos/2026".to_owned();
167        let output = "/home/dev/photos/2026".to_owned();
168        (format!("{input} {output}"), input, output)
169    }
170
171    fn law_media_source(&self) -> String {
172        "https://www.youtube.com/watch?v=VIDEO_ID".to_owned()
173    }
174
175    fn law_unrecognized_source(&self) -> String {
176        "/home/dev/music".to_owned()
177    }
178
179    fn law_lone_submission(&self) -> (String, String) {
180        (
181            "backup@nas.local:/volume1/photos/2026/".to_owned(),
182            "2026".to_owned(),
183        )
184    }
185}
186
187impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager> SyncLawFixture
188    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
189{
190    fn law_transcript(&mut self, lines: Vec<String>) {
191        self.sync.register_transcript(lines);
192    }
193
194    fn law_ran(&self, command: &Self::Command) -> bool {
195        self.sync.commands().last() == Some(command)
196    }
197}
198
199impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager> ScreenLawFixture
200    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
201{
202    type Syntax = TextScreen;
203
204    fn law_screen_syntax(&self) -> Self::Syntax {
205        TextScreen
206    }
207}
208
209/// Names the reference composition every law scenario runs against.
210pub type ReferenceLaws = LawEnv<
211    MediaSyntax,
212    ProcessingSyntax,
213    ReferenceExtractorRegistry,
214    ReferenceDownloadEnv,
215    ReferenceYoutubeEnv,
216    ManagerState,
217>;
218
219/// Names the composition the Youtube law scenarios run against.
220///
221/// Youtube retrieval consumes the reified request rather than a URL, so this composition differs
222/// from the generic one in exactly one component: the interpreter that locates a request's bytes.
223pub type ReferenceYoutubeLaws = LawEnv<
224    MediaSyntax,
225    ProcessingSyntax,
226    ReferenceExtractorRegistry,
227    ReferenceYoutubeDownloadEnv,
228    ReferenceYoutubeEnv,
229    ManagerState,
230>;
231
232impl Default for ReferenceYoutubeLaws {
233    fn default() -> Self {
234        let mut downloads = ReferenceYoutubeDownloadEnv::default();
235        downloads.register_resource(REFERENCE_MEDIA_URL, LAW_BYTES.to_vec());
236        Self {
237            syntax: MediaSyntax,
238            programs: ProcessingSyntax,
239            catalog: ReferenceExtractorRegistry::default(),
240            downloads,
241            youtube: ReferenceYoutubeEnv::default(),
242            manager: ManagerState::downloads(),
243            sync: ReferenceSyncEnv::default(),
244            applied: Vec::default(),
245            refused: None,
246        }
247    }
248}
249
250impl Default for ReferenceLaws {
251    fn default() -> Self {
252        let mut catalog = ReferenceExtractorRegistry::default();
253        for key in ["first", "second"] {
254            catalog.push(ReferenceExtractor::succeeds(
255                key,
256                "https://example.test/",
257                Extraction::Media(Media::new(
258                    key.to_owned(),
259                    InfoRecord::default(),
260                    Vec::default(),
261                )),
262            ));
263        }
264        let mut downloads = ReferenceDownloadEnv::default();
265        downloads.register_resource(LAW_RESOURCE, LAW_BYTES.to_vec());
266        Self {
267            syntax: MediaSyntax,
268            programs: ProcessingSyntax,
269            catalog,
270            downloads,
271            youtube: ReferenceYoutubeEnv::default(),
272            manager: ManagerState::downloads(),
273            sync: ReferenceSyncEnv::default(),
274            applied: Vec::default(),
275            refused: None,
276        }
277    }
278}
279
280// The composition states no sort of its own: it reads each one off the component that interprets
281// it, so the components must agree on the sorts they share.
282impl<
283    Syntax,
284    Programs,
285    Catalog,
286    Downloads,
287    Youtube,
288    Manager,
289    Value,
290    Metadata,
291    Format,
292    Artifact,
293    Media,
294    Extraction,
295    Extractor,
296    Predicate,
297    Selection,
298    Output,
299> MediaSorts for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
300where
301    Syntax: MediaSorts<
302            Value = Value,
303            Metadata = Metadata,
304            Format = Format,
305            Artifact = Artifact,
306            Media = Media,
307            Extraction = Extraction,
308            Extractor = Extractor,
309            Predicate = Predicate,
310            Selection = Selection,
311            Output = Output,
312        >,
313{
314    type Value = Value;
315    type Metadata = Metadata;
316    type Format = Format;
317    type Artifact = Artifact;
318    type Media = Media;
319    type Extraction = Extraction;
320    type Extractor = Extractor;
321    type Predicate = Predicate;
322    type Selection = Selection;
323    type Output = Output;
324}
325
326impl<
327    Syntax,
328    Programs,
329    Catalog,
330    Downloads,
331    Youtube,
332    Manager,
333    Id,
334    Requested,
335    Options,
336    Named,
337    Described,
338    Planned,
339    Entry,
340    Collection,
341> ManagerSorts for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
342where
343    Manager: ManagerSorts<
344            Id = Id,
345            Source = Requested,
346            Options = Options,
347            Output = Named,
348            Format = Described,
349            Change = Planned,
350            Entry = Entry,
351            Downloads = Collection,
352        >,
353{
354    type Id = Id;
355    type Source = Requested;
356    type Options = Options;
357    type Output = Named;
358    type Format = Described;
359    type Change = Planned;
360    type Entry = Entry;
361    type Downloads = Collection;
362}
363
364impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager, Request, Solutions> YoutubeSorts
365    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
366where
367    Youtube: YoutubeSorts<Request = Request, Solutions = Solutions>,
368{
369    type Request = Request;
370    type Solutions = Solutions;
371}
372
373impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager, Processor, Step, Program>
374    ProcessingSorts for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
375where
376    Programs: ProcessingSorts<Processor = Processor, Step = Step, Program = Program>,
377{
378    type Processor = Processor;
379    type Step = Step;
380    type Program = Program;
381}
382
383// Associated functions carry no receiver, so the view is projected rather than delegated.
384impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager> ProcessingProgramViewAlg
385    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
386where
387    Programs: ProcessingProgramViewAlg,
388{
389    fn processing_steps<'a>(program: &'a Self::Program) -> impl Iterator<Item = &'a Self::Step>
390    where
391        Self::Step: 'a,
392    {
393        Programs::processing_steps(program)
394    }
395
396    fn processing_stage(step: &Self::Step) -> ProcessingStage {
397        Programs::processing_stage(step)
398    }
399}
400
401// Retrieval is delegated by hand: the fixture states its source sort, and the component
402// interprets the default one.
403impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager, FetchError, Stream>
404    FetchStreamAlg<String> for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
405where
406    Downloads: FetchStreamAlg<Error = FetchError, Stream = Stream>,
407{
408    type Error = FetchError;
409    type Stream = Stream;
410
411    fn open_fetch(&self, source: &String) -> Result<FetchStream<Self::Stream>, Self::Error> {
412        self.downloads.open_fetch(source)
413    }
414
415    fn read_fetch(
416        &self,
417        stream: &mut Self::Stream,
418        buffer: &mut [u8],
419    ) -> Result<usize, Self::Error> {
420        self.downloads.read_fetch(stream, buffer)
421    }
422}
423
424// The context records its own application trace, so these it states rather than delegates.
425impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager> ProcessingApplyAlg
426    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
427where
428    Programs: ProcessingSorts<Processor = ProcessorId, Step = ProcessingStep>,
429{
430    type Error = ReferenceProcessingRefusal;
431
432    fn apply_processing_step(&mut self, step: &Self::Step) -> Result<(), Self::Error> {
433        let processor = step.processor.0.clone();
434        if self.refused.as_ref() == Some(&processor) {
435            return Err(ReferenceProcessingRefusal(processor));
436        }
437        self.applied.push(processor);
438        Ok(())
439    }
440}
441
442impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager> ProcessingLawFixture
443    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
444{
445    fn law_applied(&self) -> Vec<String> {
446        self.applied.clone()
447    }
448
449    fn refuse_law_processor(&mut self, id: &str) {
450        self.refused = Some(id.to_owned());
451    }
452}
453
454// The fixture reaches this context's own catalog, so it is stated for the reference composition.
455impl ExtractionLawFixture for ReferenceLaws {
456    fn accepted_law_url(&self) -> String {
457        "https://example.test/video".to_owned()
458    }
459
460    fn unsupported_law_url(&self) -> String {
461        "https://unsupported.test/video".to_owned()
462    }
463
464    fn law_applied_extractors(&self) -> Vec<String> {
465        self.catalog
466            .applications()
467            .iter()
468            .map(|key| key.0.clone())
469            .collect()
470    }
471}
472
473// A format states where its bytes rest, so locating one is reading that observation back.
474impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager> FormatSourceAlg<String>
475    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
476where
477    Syntax: MediaSorts<Format = Format>,
478{
479    fn format_source(&self, format: &Format) -> Option<String> {
480        format.source().map(str::to_owned)
481    }
482}
483
484impl MediaProgramLawFixture for ReferenceLaws {
485    fn located_law_source(&self) -> String {
486        LAW_RESOURCE.to_owned()
487    }
488
489    fn law_extension(&self) -> String {
490        "bin".to_owned()
491    }
492
493    fn expected_law_bytes(&self) -> Vec<u8> {
494        LAW_BYTES.to_vec()
495    }
496
497    fn law_published_bytes(&self, path: &Path) -> Option<Vec<u8>> {
498        self.downloads.file(path)
499    }
500}
501
502impl DownloadLawFixture for ReferenceLaws {
503    type Source = String;
504
505    fn law_source(&self) -> Self::Source {
506        LAW_RESOURCE.to_owned()
507    }
508    fn law_destination(&self) -> PathBuf {
509        PathBuf::from("published.bin")
510    }
511    fn law_bytes(&self) -> Vec<u8> {
512        LAW_BYTES.to_vec()
513    }
514    fn forget_law_resource(&mut self) {
515        self.downloads = ReferenceDownloadEnv::default();
516    }
517    fn refuse_law_publication(&mut self) {
518        self.downloads.refuse_publication();
519    }
520    fn law_progress(&self) -> Vec<u64> {
521        self.downloads
522            .progress()
523            .iter()
524            .map(|observed| observed.downloaded)
525            .collect()
526    }
527    fn law_terminal_events(&self) -> usize {
528        self.downloads.events().len()
529    }
530    fn law_published(&self) -> Option<Vec<u8>> {
531        self.downloads.file(&self.law_destination())
532    }
533    fn law_abandoned(&self) -> usize {
534        self.downloads.abandoned()
535    }
536}
537
538// The Youtube fixtures reach this context's own seeded player, so they are stated wherever that
539// interpreter is the Youtube component.
540impl<Syntax, Programs, Catalog, Downloads, Manager> YoutubeChallengeLawFixture
541    for LawEnv<Syntax, Programs, Catalog, Downloads, ReferenceYoutubeEnv, Manager>
542{
543    fn solve_law_challenge(&mut self, challenge: YoutubeChallenge, solution: &str) {
544        self.youtube.solve(challenge, solution);
545    }
546
547    fn law_challenge_applications(&self) -> Vec<Vec<YoutubeChallenge>> {
548        self.youtube.applications()
549    }
550}
551
552impl<Syntax, Programs, Catalog, Manager> YoutubeLawFixture
553    for LawEnv<Syntax, Programs, Catalog, ReferenceYoutubeDownloadEnv, ReferenceYoutubeEnv, Manager>
554{
555    fn law_watch_url(&self) -> String {
556        REFERENCE_WATCH_URL.to_owned()
557    }
558
559    fn law_video_id(&self) -> String {
560        REFERENCE_VIDEO_ID.to_owned()
561    }
562
563    fn law_watch_bytes(&self) -> Vec<u8> {
564        ReferenceYoutubeEnv::watch_bytes()
565    }
566
567    fn law_player_bytes(&self) -> Vec<u8> {
568        ReferenceYoutubeEnv::player_bytes()
569    }
570
571    fn law_executed_requests(&self) -> Vec<YoutubeRequest> {
572        self.youtube.executed()
573    }
574
575    fn law_retrieved_requests(&self) -> Vec<YoutubeRequest> {
576        self.downloads.opened()
577    }
578}
579
580impl MediaProgramLawFixture for ReferenceYoutubeLaws {
581    fn located_law_source(&self) -> String {
582        REFERENCE_MEDIA_URL.to_owned()
583    }
584
585    fn law_extension(&self) -> String {
586        REFERENCE_MEDIA_EXTENSION.to_owned()
587    }
588
589    fn expected_law_bytes(&self) -> Vec<u8> {
590        LAW_BYTES.to_vec()
591    }
592
593    fn law_published_bytes(&self, path: &Path) -> Option<Vec<u8>> {
594        self.downloads.file(path)
595    }
596}
597
598// A Youtube format states where its bytes rest, and retrieving them is a media request.
599impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager> FormatSourceAlg<YoutubeRequest>
600    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
601where
602    Syntax: MediaSorts<Format = Format>,
603    Youtube: YoutubeSorts<Request = YoutubeRequest> + YoutubeRequestAlg,
604{
605    fn format_source(&self, format: &Format) -> Option<YoutubeRequest> {
606        format.source().map(|url| self.youtube.media_request(url))
607    }
608}
609
610impl<Syntax, Programs, Catalog, Downloads, Youtube, Manager, FetchError, Stream>
611    FetchStreamAlg<YoutubeRequest>
612    for LawEnv<Syntax, Programs, Catalog, Downloads, Youtube, Manager>
613where
614    Downloads: FetchStreamAlg<YoutubeRequest, Error = FetchError, Stream = Stream>,
615{
616    type Error = FetchError;
617    type Stream = Stream;
618
619    fn open_fetch(
620        &self,
621        request: &YoutubeRequest,
622    ) -> Result<FetchStream<Self::Stream>, Self::Error> {
623        self.downloads.open_fetch(request)
624    }
625
626    fn read_fetch(
627        &self,
628        stream: &mut Self::Stream,
629        buffer: &mut [u8],
630    ) -> Result<usize, Self::Error> {
631        self.downloads.read_fetch(stream, buffer)
632    }
633}
634
635// The downloads collection is the memory interpreter's own manager state, so it observes itself.
636impl DownloadsLawFixture for MemoryManager {
637    fn law_collected_sources(&self, downloads: &Self::Downloads) -> Vec<String> {
638        downloads
639            .queue()
640            .iter()
641            .map(|entry| QueueEntry::label(entry).to_owned())
642            .collect()
643    }
644}