1use 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#[derive(Debug, thiserror::Error)]
30#[error("processor {0} refused")]
31pub struct ReferenceProcessingRefusal(String);
32
33#[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
185pub type ReferenceLaws = LawEnv<
187 MediaSyntax,
188 ProcessingSyntax,
189 ReferenceExtractorRegistry,
190 ReferenceDownloadEnv,
191 ReferenceYoutubeEnv,
192 ManagerState,
193>;
194
195pub 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
252impl<
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
355impl<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
373impl<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
392impl<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
422impl 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
437impl<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
498impl<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
558impl<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
587impl 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}