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