1use 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#[derive(Debug, thiserror::Error)]
32#[error("processor {0} refused")]
33pub struct ReferenceProcessingRefusal(String);
34
35#[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
209pub type ReferenceLaws = LawEnv<
211 MediaSyntax,
212 ProcessingSyntax,
213 ReferenceExtractorRegistry,
214 ReferenceDownloadEnv,
215 ReferenceYoutubeEnv,
216 ManagerState,
217>;
218
219pub 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
280impl<
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
383impl<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
401impl<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
424impl<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
454impl 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
473impl<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
538impl<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
598impl<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
635impl 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}