1use crate::*;
9use alux_ext::ext;
10use anyhow::{Result, bail};
11use rsynko_download::*;
12use std::fmt::{Debug, Display};
13use std::path::{Path, PathBuf};
14
15pub trait ExtractionLawFixture {
17 fn accepted_law_url(&self) -> String;
19 fn unsupported_law_url(&self) -> String;
21 fn law_applied_extractors(&self) -> Vec<String>;
23}
24
25#[ext(name = ObservationLaws)]
27pub impl<This> This
28where
29 This: MetadataAlg,
30{
31 fn observation_laws(&self) -> Result<()>
37 where
38 This::Metadata: PartialEq + Debug,
39 This::Value: Clone,
40 {
41 if self.metadata([]) != self.empty_metadata() {
42 bail!("neutrality violated: a record of no fields differs from the empty record");
43 }
44 let stated = self.string_metadata("first");
45 if self.metadata([("title".to_owned(), stated.clone())])
46 != self.text_metadata("title", "first")
47 {
48 bail!("single-field construction disagrees with its derived form");
49 }
50 let replaced = self.metadata([
51 ("title".to_owned(), stated),
52 ("title".to_owned(), self.string_metadata("second")),
53 ]);
54 if replaced != self.text_metadata("title", "second") {
55 bail!("replacement violated: a later equal key did not replace the earlier field");
56 }
57 Ok(())
58 }
59}
60
61#[ext(name = SelectionLaws)]
63pub impl<This> This
64where
65 This: MetadataAlg
66 + FormatAlg
67 + FormatPredicateAlg
68 + FormatSelectionAlg
69 + FormatPredicateMatchAlg
70 + FormatSelectionApplyAlg,
71{
72 fn selection_laws(&self) -> Result<()>
78 where
79 This::Predicate: Clone,
80 {
81 let catalog = [false, true, true].map(|video| {
82 self.format(
83 if video { "video" } else { "audio" },
84 self.metadata([(FORMAT_HAS_VIDEO.to_owned(), self.boolean_metadata(video))]),
85 )
86 });
87 self.check_selection_against(&catalog, &self.has_video())?;
88 self.check_selection_against(&catalog, &self.format_extension("never"))?;
89
90 let sized = [360_i64, 720, 1080].map(|height| {
92 self.format(
93 "sized",
94 self.metadata([("height".to_owned(), self.integer_metadata(height))]),
95 )
96 });
97 self.check_selection_against(&sized, &self.at_most("height", 720.0))?;
98 self.check_selection_against(&sized, &self.greater_than("height", 720.0))?;
99 self.check_selection_against(&catalog, &self.at_most("height", 720.0))?;
101 self.check_selection_against(&catalog, &self.observed_format("height"))
102 }
103
104 fn check_selection_against(
110 &self,
111 catalog: &[This::Format],
112 predicate: &This::Predicate,
113 ) -> Result<()>
114 where
115 This::Predicate: Clone,
116 {
117 let matching: Vec<&This::Format> = catalog
118 .iter()
119 .filter(|format| self.format_matches(predicate, format))
120 .collect();
121 let best = self.select_formats(catalog, &self.best_format(predicate.clone()));
122 let worst = self.select_formats(catalog, &self.worst_format(predicate.clone()));
123
124 let (Some(first), Some(last)) = (matching.first(), matching.last()) else {
125 if best.is_some() || worst.is_some() {
126 bail!("selection chose a format although nothing matches the predicate");
127 }
128 return Ok(());
129 };
130 if !chose(best.as_deref(), last) {
131 bail!("Best did not choose the most preferred matching format");
132 }
133 if !chose(worst.as_deref(), first) {
134 bail!("Worst did not choose the least preferred matching format");
135 }
136
137 let merged = self.select_formats(
138 catalog,
139 &self.merge_formats([
140 self.best_format(predicate.clone()),
141 self.worst_format(predicate.clone()),
142 ]),
143 );
144 let Some(merged) = merged else {
145 bail!("Merge failed although every child succeeded");
146 };
147 if merged.len() != 2 || !std::ptr::eq(merged[0], *last) || !std::ptr::eq(merged[1], *first)
148 {
149 bail!("Merge did not preserve child-program order");
150 }
151
152 let fallback = self.select_formats(
153 catalog,
154 &self.fallback_formats([
155 self.best_format(predicate.clone()),
156 self.worst_format(predicate.clone()),
157 ]),
158 );
159 if !chose(fallback.as_deref(), last) {
160 bail!("Fallback was not left-biased");
161 }
162 Ok(())
163 }
164}
165
166fn chose<Format>(selected: Option<&[&Format]>, expected: &Format) -> bool {
168 selected.is_some_and(|chosen| chosen.len() == 1 && std::ptr::eq(chosen[0], expected))
169}
170
171#[ext(name = FormatLaws)]
173pub impl<This> This
174where
175 This: MetadataAlg + FormatAlg + FormatViewAlg,
176{
177 fn format_laws(&self) -> Result<()> {
189 let source = "https://media.example/a.mp4";
190 let stated = self.format(
191 "18",
192 self.metadata([
193 (FORMAT_SOURCE.to_owned(), self.string_metadata(source)),
194 (FORMAT_EXTENSION.to_owned(), self.string_metadata("mp4")),
195 ]),
196 );
197 if self.format_text(&stated, FORMAT_SOURCE) != Some(source) {
198 bail!("a stated source is not the one the format is observed under");
199 }
200 if self.format_text(&stated, FORMAT_EXTENSION) != Some("mp4") {
201 bail!("stating a source disturbed the stated extension");
202 }
203 if let Some(observed) = self.format_text(&stated, "quality") {
204 bail!("an unstated observation was observed as {observed}");
205 }
206 let bare = self.format("18", self.empty_metadata());
207 if self.format_text(&bare, FORMAT_SOURCE).is_some() {
208 bail!("a format stating nothing still locates its bytes");
209 }
210 Ok(())
211 }
212}
213
214#[ext(name = ArtifactLaws)]
216pub impl<This> This
217where
218 This: MetadataAlg + ArtifactAlg,
219{
220 fn artifact_laws(&self) -> Result<()>
231 where
232 This::Artifact: PartialEq + Debug,
233 This::Metadata: Clone,
234 {
235 let resting = self.metadata([(
236 ARTIFACT_LOCATION.to_owned(),
237 self.string_metadata("temporary.part"),
238 )]);
239 let stated = self.artifact("media", ArtifactKind::Media, resting.clone());
240 if stated != self.artifact("media", ArtifactKind::Media, resting.clone()) {
241 bail!("definition is not a function of identity, role, and observations");
242 }
243 for (name, other) in [
244 (
245 "identity",
246 self.artifact("subtitle", ArtifactKind::Media, resting.clone()),
247 ),
248 (
249 "role",
250 self.artifact("media", ArtifactKind::Subtitle, resting),
251 ),
252 (
253 "observations",
254 self.artifact("media", ArtifactKind::Media, self.empty_metadata()),
255 ),
256 ] {
257 if stated == other {
258 bail!("artifact {name} is not significant: {stated:?} equals {other:?}");
259 }
260 }
261 Ok(())
262 }
263}
264
265pub trait ProcessingLawFixture {
267 fn law_applied(&self) -> Vec<String>;
269 fn refuse_law_processor(&mut self, id: &str);
271}
272
273#[ext(name = ProcessingApplicationLaws)]
275pub impl<This> This
276where
277 This:
278 ProcessingProgramAlg + ProcessingProgramViewAlg + ProcessingApplyAlg + ProcessingLawFixture,
279{
280 fn processing_stage_laws(&mut self) -> Result<()>
286 where
287 This::Error: Debug,
288 {
289 let program = self.processing([
290 self.processing_step(ProcessingStage::PreProcess, self.processor("before")),
291 self.processing_step(ProcessingStage::AfterMove, self.processor("first")),
292 self.processing_step(ProcessingStage::AfterMove, self.processor("second")),
293 ]);
294 if let Err(error) = self.run_processing_stage(&program, ProcessingStage::AfterMove) {
295 bail!("stage interpretation failed: {error:?}");
296 }
297 let applied = self.law_applied();
298 if applied != ["first", "second"] {
299 bail!("stage selection did not preserve relative declaration order: {applied:?}");
300 }
301 Ok(())
302 }
303
304 fn processing_failure_laws(&mut self) -> Result<()> {
310 let program = self.processing([
311 self.processing_step(ProcessingStage::PostProcess, self.processor("first")),
312 self.processing_step(ProcessingStage::PostProcess, self.processor("refused")),
313 self.processing_step(ProcessingStage::PostProcess, self.processor("never")),
314 ]);
315 self.refuse_law_processor("refused");
316 if self.run_processing_program(&program).is_ok() {
317 bail!("a refused step did not fail the program");
318 }
319 let applied = self.law_applied();
320 if applied != ["first"] {
321 bail!("failure did not stop interpretation before later steps: {applied:?}");
322 }
323 Ok(())
324 }
325}
326
327#[ext(name = ProcessingLaws)]
329pub impl<This> This
330where
331 This: ProcessingProgramAlg + ProcessingProgramViewAlg,
332{
333 fn processing_laws(&self) -> Result<()>
342 where
343 This::Step: Clone + PartialEq + Debug,
344 {
345 let authored: Vec<This::Step> = [
346 (ProcessingStage::PostProcess, "first"),
347 (ProcessingStage::AfterMove, "second"),
348 ]
349 .into_iter()
350 .map(|(stage, name)| self.processing_step(stage, self.processor(name)))
351 .collect();
352
353 if Self::processing_steps(&self.processing([]))
354 .next()
355 .is_some()
356 {
357 bail!("the empty program is not empty");
358 }
359 let program = self.processing(authored.iter().cloned());
360 let declared: Vec<This::Step> = Self::processing_steps(&program).cloned().collect();
361 if declared != authored {
362 bail!("construction did not preserve declaration order: {declared:?}");
363 }
364 for (side, composed) in [
365 (
366 "left",
367 self.then_processing(
368 self.processing([]),
369 self.processing(authored.iter().cloned()),
370 ),
371 ),
372 (
373 "right",
374 self.then_processing(
375 self.processing(authored.iter().cloned()),
376 self.processing([]),
377 ),
378 ),
379 ] {
380 let observed: Vec<This::Step> = Self::processing_steps(&composed).cloned().collect();
381 if observed != authored {
382 bail!("the empty program is not an identity on the {side}: {observed:?}");
383 }
384 }
385 let doubled = self.then_processing(
386 self.processing(authored.iter().cloned()),
387 self.processing(authored.iter().cloned()),
388 );
389 let observed: Vec<This::Step> = Self::processing_steps(&doubled).cloned().collect();
390 let expected: Vec<This::Step> = authored.iter().chain(&authored).cloned().collect();
391 if observed != expected {
392 bail!("concatenation is not sequential composition: {observed:?}");
393 }
394 Ok(())
395 }
396}
397
398#[ext(name = OutputNameLaws)]
400pub impl<This> This
401where
402 This: OutputNameAlg,
403{
404 fn output_naming_laws(&self) -> Result<()> {
410 let named = portable_file_name(Some("A/B"), "id", Some("mp4"));
411 if named != portable_file_name(Some("A/B"), "id", Some("mp4")) {
412 bail!("naming is not a function of title, fallback, and extension");
413 }
414 if portable_file_name(None, "id", Some("mp4"))
415 != portable_file_name(Some(" "), "id", Some("mp4"))
416 {
417 bail!("a blank title did not select the stated fallback identity");
418 }
419 for candidate in [&named, &portable_user_file_name("a/b.mp4", Some("mp4"))] {
420 if candidate.components().count() != 1 {
421 bail!(
422 "naming produced a path rather than one file component: {}",
423 candidate.display()
424 );
425 }
426 }
427 if portable_user_file_name("edited.mp4", Some("bin"))
428 .extension()
429 .and_then(|value| value.to_str())
430 != Some("mp4")
431 {
432 bail!("an edited name did not keep its valid explicit extension");
433 }
434 Ok(())
435 }
436}
437
438#[ext(name = ExtractionLaws)]
440pub impl<This> This
441where
442 This: ExtractionCatalogAlg + ExtractionApplyAlg + ExtractionLawFixture,
443{
444 fn extraction_laws(&self) -> Result<()>
450 where
451 This::Extractor: PartialEq + Debug,
452 This::Extraction: Debug,
453 This::Error: Debug,
454 {
455 let accepted = self.accepted_law_url();
456 let unsupported = self.unsupported_law_url();
457 let expected = self
458 .extractor_keys()
459 .find(|key| self.extractor_accepts(key, &accepted));
460 let Some(expected) = expected else {
461 bail!("the fixture URL {accepted} is accepted by no catalog entry");
462 };
463 match self.choose_extractor(&accepted) {
464 Some(chosen) if chosen == expected => {}
465 other => bail!("first-accepting choice violated: chose {other:?}"),
466 }
467 if let Some(chosen) = self.choose_extractor(&unsupported) {
468 bail!("unsupported input chose {chosen:?}, expected no extractor");
469 }
470 if let Err(error) = self.extract_url(&accepted) {
471 bail!("accepted input failed to extract: {error:?}");
472 }
473 let applied = self.law_applied_extractors();
474 if applied.len() != 1 {
475 bail!("extracting a supported URL applied {applied:?}, expected exactly one extractor");
476 }
477 match self.extract_url(&unsupported) {
478 Err(ExtractUrlError::Unsupported { url }) if url == unsupported => {}
479 other => bail!("unsupported input denoted {other:?}, expected an unsupported error"),
480 }
481 if self.law_applied_extractors() != applied {
482 bail!("extracting an unsupported URL applied an extractor");
483 }
484 Ok(())
485 }
486}
487
488#[ext(name = MediaProgramLaws)]
490pub impl<This, Source, Event, Progress> This
491where
492 This: MetadataAlg
493 + FormatAlg
494 + FormatViewAlg
495 + ExtractionAlg
496 + ExtractionViewAlg
497 + MediaViewAlg
498 + FormatPredicateAlg
499 + FormatSelectionAlg
500 + FormatSelectionApplyAlg
501 + FormatSourceAlg<Source>
502 + FetchStreamAlg<Source, Error: Display>
503 + AtomicPublishAlg<Error: Display>
504 + DownloadObservationAlg<Event = Event, Progress = Progress>
505 + DownloadProgressAlg<Progress = Progress>
506 + DownloadReportAlg<Event = Event>
507 + MediaProgramLawFixture,
508{
509 fn media_program_laws(&mut self) -> Result<()> {
522 let format = self.format(
523 "18",
524 self.metadata([
525 (
526 FORMAT_SOURCE.to_owned(),
527 self.string_metadata(self.located_law_source()),
528 ),
529 (
530 FORMAT_EXTENSION.to_owned(),
531 self.string_metadata(self.law_extension()),
532 ),
533 ]),
534 );
535 let extraction = self.media("law-media", self.empty_metadata(), [format]);
536 let selection = self.best_format(self.any_format());
537
538 let published =
539 match self.download_extraction(extraction, &selection, &OutputTarget::MediaId) {
540 Ok(path) => path,
541 Err(error) => bail!("the composed program failed: {error}"),
542 };
543 let expected = PathBuf::from(format!("law-media.{}", self.law_extension()));
544 if published != expected {
545 bail!(
546 "naming derived {} rather than {}",
547 published.display(),
548 expected.display()
549 );
550 }
551 if self.law_published_bytes(&published).as_deref() != Some(&self.expected_law_bytes()[..]) {
552 bail!("the selected format's source did not reach download unchanged");
553 }
554
555 let reference = self.url_reference("https://example.test/other", None, false);
556 match self.download_extraction(reference, &selection, &OutputTarget::MediaId) {
557 Err(MediaDownloadError::NotSingleMedia) => Ok(()),
558 other => bail!("a non-media result was not refused: {}", describe(&other)),
559 }
560 }
561}
562
563pub trait MediaProgramLawFixture {
565 fn located_law_source(&self) -> String;
567 fn law_extension(&self) -> String;
569 fn expected_law_bytes(&self) -> Vec<u8>;
571 fn law_published_bytes(&self, path: &Path) -> Option<Vec<u8>>;
573}
574
575fn describe<Ok, Error: Display>(outcome: &Result<Ok, Error>) -> String {
577 match outcome {
578 Ok(_) => "a successful download".to_owned(),
579 Err(error) => error.to_string(),
580 }
581}