feat: added entry <-> resource relation (#927)

This commit is contained in:
Kyrch
2025-08-18 03:23:37 -03:00
committed by GitHub
parent 9b08e233e3
commit eb265c839b
16 changed files with 377 additions and 4 deletions
@@ -25,7 +25,7 @@ class AttachResourceAction
foreach ($sites as $resourceSite) {
$url = Arr::get($fields, $resourceSite->name);
if (empty($url)) {
if (blank($url)) {
continue;
}
+27
View File
@@ -7,10 +7,12 @@ namespace App\Enums\Models\Wiki;
use App\Concerns\Enums\LocalizesName;
use App\Contracts\Models\HasResources;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Artist;
use App\Models\Wiki\Song;
use App\Models\Wiki\Studio;
use App\Rules\Wiki\Resource\AnimeResourceLinkFormatRule;
use App\Rules\Wiki\Resource\AnimeThemeEntryResourceLinkFormatRule;
use App\Rules\Wiki\Resource\ArtistResourceLinkFormatRule;
use App\Rules\Wiki\Resource\SongResourceLinkFormatRule;
use App\Rules\Wiki\Resource\StudioResourceLinkFormatRule;
@@ -197,6 +199,7 @@ enum ResourceSite: int implements HasLabel
{
return match (true) {
$model instanceof Anime => new AnimeResourceLinkFormatRule($this),
$model instanceof AnimeThemeEntry => new AnimeThemeEntryResourceLinkFormatRule($this),
$model instanceof Artist => new ArtistResourceLinkFormatRule($this),
$model instanceof Song => new SongResourceLinkFormatRule($this),
$model instanceof Studio => new StudioResourceLinkFormatRule($this),
@@ -232,6 +235,9 @@ enum ResourceSite: int implements HasLabel
ResourceSite::WIKI,
ResourceSite::LIVECHART,
],
AnimeThemeEntry::class => [
ResourceSite::YOUTUBE,
],
Artist::class => [
ResourceSite::X,
ResourceSite::ANIDB,
@@ -297,6 +303,13 @@ enum ResourceSite: int implements HasLabel
};
}
if ($modelClass === AnimeThemeEntry::class) {
return match ($this) {
ResourceSite::YOUTUBE => "https://www.youtube.com/watch?v=$slug",
default => null,
};
}
if ($modelClass === Artist::class) {
return match ($this) {
ResourceSite::X => "https://x.com/$slug",
@@ -377,6 +390,13 @@ enum ResourceSite: int implements HasLabel
};
}
if ($model instanceof AnimeThemeEntry) {
return match ($this) {
ResourceSite::YOUTUBE => '/^https:\/\/www\.youtube\.com\/(watch)\?v=([\w-]+)$/',
default => '/^$/',
};
}
if ($model instanceof Artist) {
return match ($this) {
ResourceSite::X => '/^https:\/\/(x)\.com\/(\w+)$/',
@@ -449,6 +469,13 @@ enum ResourceSite: int implements HasLabel
};
}
if ($modelClass === AnimeThemeEntry::class) {
return match ($this) {
ResourceSite::YOUTUBE => '/^https:\/\/www\.youtube\.com\/watch\?v=[\w-]+$/',
default => '/$.^/',
};
}
if ($modelClass === Artist::class) {
return match ($this) {
ResourceSite::X => '/^https:\/\/x\.com\/\w+$/',
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Anime\Theme\Entry;
use App\Enums\Models\Wiki\ResourceSite;
use App\Filament\Actions\Models\Wiki\AttachResourceAction;
class AttachEntryResourceAction extends AttachResourceAction
{
/**
* The default name of the action.
*/
public static function getDefaultName(): ?string
{
return 'attach-entry-resource';
}
/**
* Initial setup for the action.
*/
protected function setUp(): void
{
parent::setUp();
$this->sites([
ResourceSite::YOUTUBE,
]);
}
}
@@ -4,7 +4,10 @@ declare(strict_types=1);
namespace App\Filament\Resources\Wiki\Anime\Theme;
use App\Actions\Models\Wiki\AttachResourceAction;
use App\Enums\Models\Wiki\ResourceSite;
use App\Enums\Models\Wiki\ThemeType;
use App\Filament\Actions\Models\Wiki\Anime\Theme\Entry\AttachEntryResourceAction;
use App\Filament\Components\Columns\BelongsToColumn;
use App\Filament\Components\Columns\TextColumn;
use App\Filament\Components\Fields\BelongsTo;
@@ -16,6 +19,7 @@ use App\Filament\Components\Filters\TextFilter;
use App\Filament\Components\Infolist\BelongsToEntry;
use App\Filament\Components\Infolist\TextEntry;
use App\Filament\Components\Infolist\TimestampSection;
use App\Filament\RelationManagers\Wiki\ResourceRelationManager;
use App\Filament\Resources\BaseResource;
use App\Filament\Resources\Wiki\Anime as AnimeResource;
use App\Filament\Resources\Wiki\Anime\Theme as ThemeResource;
@@ -23,9 +27,11 @@ use App\Filament\Resources\Wiki\Anime\Theme\Entry\Pages\ListEntries;
use App\Filament\Resources\Wiki\Anime\Theme\Entry\Pages\ViewEntry;
use App\Filament\Resources\Wiki\Anime\Theme\Entry\RelationManagers\VideoEntryRelationManager;
use App\Filament\Resources\Wiki\Anime\Theme\RelationManagers\EntryThemeRelationManager;
use App\Filament\Resources\Wiki\Song as SongResource;
use App\Models\Wiki\Anime\AnimeTheme as ThemeModel;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry as EntryModel;
use App\Rules\Wiki\Resource\AnimeThemeEntryResourceLinkFormatRule;
use Filament\Forms\Components\Checkbox;
use Filament\Infolists\Components\IconEntry;
use Filament\Resources\RelationManagers\RelationGroup;
@@ -37,6 +43,7 @@ use Filament\Tables\Filters\Filter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Uri;
class Entry extends BaseResource
{
@@ -129,7 +136,11 @@ class Entry extends BaseResource
$query = parent::getEloquentQuery();
// Necessary to prevent lazy loading when loading related resources
return $query->with([AnimeThemeEntry::RELATION_ANIME_SHALLOW, AnimeThemeEntry::RELATION_THEME]);
return $query->with([
AnimeThemeEntry::RELATION_ANIME_SHALLOW,
AnimeThemeEntry::RELATION_SONG,
AnimeThemeEntry::RELATION_THEME,
]);
}
/**
@@ -185,6 +196,21 @@ class Entry extends BaseResource
->label(__('filament.fields.anime_theme_entry.notes.name'))
->helperText(__('filament.fields.anime_theme_entry.notes.help'))
->maxLength(192),
TextInput::make(ResourceSite::YOUTUBE->name)
->label(ResourceSite::YOUTUBE->localize())
->helperText(__('filament.fields.anime_theme_entry.youtube.help'))
->url()
->maxLength(255)
->rule(new AnimeThemeEntryResourceLinkFormatRule(ResourceSite::YOUTUBE))
->uri()
->saveRelationshipsUsing(function (EntryModel $record, AttachResourceAction $action, Uri $state) {
$fields = [
ResourceSite::YOUTUBE->name => $state,
];
$action->handle($record, $fields, [ResourceSite::YOUTUBE]);
}),
])
->columns(1);
}
@@ -222,6 +248,9 @@ class Entry extends BaseResource
->label(__('filament.fields.anime_theme_entry.notes.name'))
->limit(50)
->tooltip(fn (TextColumn $column) => $column->getState()),
BelongsToColumn::make(AnimeThemeEntry::RELATION_SONG, SongResource::class)
->hiddenOn(EntryThemeRelationManager::class),
])
->searchable();
}
@@ -241,6 +270,8 @@ class Entry extends BaseResource
BelongsToEntry::make(EntryModel::RELATION_THEME, ThemeResource::class, true),
BelongsToEntry::make(EntryModel::RELATION_SONG, SongResource::class, true),
TextEntry::make(EntryModel::ATTRIBUTE_VERSION)
->label(__('filament.fields.anime_theme_entry.version.name')),
@@ -280,6 +311,7 @@ class Entry extends BaseResource
return [
RelationGroup::make(static::getModelLabel(), [
VideoEntryRelationManager::class,
ResourceRelationManager::class,
...parent::getBaseRelations(),
]),
@@ -315,6 +347,18 @@ class Entry extends BaseResource
];
}
/**
* Get the actions available for the resource.
*
* @return array<int, \Filament\Actions\Action|\Filament\Actions\ActionGroup>
*/
public static function getRecordActions(): array
{
return [
AttachEntryResourceAction::make(),
];
}
/**
* Get the pages available for the resource.
*
@@ -16,6 +16,7 @@ use App\Filament\Resources\Wiki\ExternalResource\Pages\ListExternalResources;
use App\Filament\Resources\Wiki\ExternalResource\Pages\ViewExternalResource;
use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\AnimeResourceRelationManager;
use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\ArtistResourceRelationManager;
use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\EntryResourceRelationManager;
use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\SongResourceRelationManager;
use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\StudioResourceRelationManager;
use App\Models\Wiki\ExternalResource as ExternalResourceModel;
@@ -195,6 +196,7 @@ class ExternalResource extends BaseResource
return [
RelationGroup::make(static::getModelLabel(), [
AnimeResourceRelationManager::class,
EntryResourceRelationManager::class,
ArtistResourceRelationManager::class,
SongResourceRelationManager::class,
StudioResourceRelationManager::class,
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\Wiki\ExternalResource\RelationManagers;
use App\Filament\Components\Columns\TextColumn;
use App\Filament\Components\Fields\TextInput;
use App\Filament\RelationManagers\Wiki\Anime\Theme\EntryRelationManager;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\ExternalResource;
use App\Pivots\Morph\Resourceable;
use Filament\Schemas\Components\Component;
use Filament\Tables\Columns\Column;
use Filament\Tables\Table;
class EntryResourceRelationManager extends EntryRelationManager
{
/**
* The relationship the relation manager corresponds to.
*/
protected static string $relationship = ExternalResource::RELATION_ANIMETHEMEENTRIES;
/**
* Get the pivot components of the relation.
*
* @return Component[]
*/
public function getPivotComponents(): array
{
return [
TextInput::make(Resourceable::ATTRIBUTE_AS)
->label(__('filament.fields.resourceable.as.name'))
->helperText(__('filament.fields.resourceable.as.help')),
];
}
/**
* Get the pivot columns of the relation.
*
* @return Column[]
*/
public function getPivotColumns(): array
{
return [
TextColumn::make(Resourceable::ATTRIBUTE_AS)
->label(__('filament.fields.resourceable.as.name')),
];
}
/**
* The index page of the resource.
*/
public function table(Table $table): Table
{
return parent::table(
$table
->inverseRelationship(AnimeThemeEntry::RELATION_RESOURCES)
);
}
}
@@ -16,11 +16,14 @@ use App\GraphQL\Definition\Fields\Wiki\Anime\Theme\Entry\AnimeThemeEntryNsfwFiel
use App\GraphQL\Definition\Fields\Wiki\Anime\Theme\Entry\AnimeThemeEntrySpoilerField;
use App\GraphQL\Definition\Fields\Wiki\Anime\Theme\Entry\AnimeThemeEntryVersionField;
use App\GraphQL\Definition\Types\EloquentType;
use App\GraphQL\Definition\Types\Pivot\Morph\ResourceableType;
use App\GraphQL\Definition\Types\Pivot\Wiki\AnimeThemeEntryVideoType;
use App\GraphQL\Definition\Types\Wiki\Anime\AnimeThemeType;
use App\GraphQL\Definition\Types\Wiki\ExternalResourceType;
use App\GraphQL\Definition\Types\Wiki\VideoType;
use App\GraphQL\Support\Relations\BelongsToManyRelation;
use App\GraphQL\Support\Relations\BelongsToRelation;
use App\GraphQL\Support\Relations\MorphToManyRelation;
use App\GraphQL\Support\Relations\Relation;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
@@ -45,6 +48,7 @@ class AnimeThemeEntryType extends EloquentType implements ReportableType
new BelongsToRelation(new AnimeThemeType(), AnimeThemeEntry::RELATION_THEME)
->notNullable(),
new BelongsToManyRelation($this, VideoType::class, AnimeThemeEntry::RELATION_VIDEOS, AnimeThemeEntryVideoType::class),
new MorphToManyRelation($this, ExternalResourceType::class, AnimeThemeEntry::RELATION_RESOURCES, ResourceableType::class),
];
}
@@ -7,6 +7,7 @@ namespace App\Models\Wiki\Anime\Theme;
use App\Concerns\Models\Reportable;
use App\Concerns\Models\SoftDeletes;
use App\Contracts\Http\Api\InteractsWithSchema;
use App\Contracts\Models\HasResources;
use App\Contracts\Models\SoftDeletable;
use App\Enums\Models\Wiki\ThemeType;
use App\Events\Wiki\Anime\Theme\Entry\EntryCreated;
@@ -20,7 +21,9 @@ use App\Http\Resources\Pivot\Wiki\Resource\AnimeThemeEntryVideoResource;
use App\Models\BaseModel;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Video;
use App\Pivots\Morph\Resourceable;
use App\Pivots\Wiki\AnimeThemeEntryVideo;
use App\Scopes\WithoutInsertSongScope;
use Database\Factories\Wiki\Anime\Theme\AnimeThemeEntryFactory;
@@ -29,6 +32,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Znck\Eloquent\Relations\BelongsToThrough;
@@ -43,6 +47,7 @@ use Znck\Eloquent\Traits\BelongsToThrough as ZnckBelongsToThrough;
* @property string|null $episodes
* @property string|null $notes
* @property bool $nsfw
* @property Collection<int, ExternalResource> $resources
* @property bool $spoiler
* @property int $theme_id
* @property int|null $version
@@ -50,7 +55,7 @@ use Znck\Eloquent\Traits\BelongsToThrough as ZnckBelongsToThrough;
*
* @method static AnimeThemeEntryFactory factory(...$parameters)
*/
class AnimeThemeEntry extends BaseModel implements InteractsWithSchema, SoftDeletable
class AnimeThemeEntry extends BaseModel implements HasResources, InteractsWithSchema, SoftDeletable
{
use HasFactory;
use Reportable;
@@ -70,6 +75,7 @@ class AnimeThemeEntry extends BaseModel implements InteractsWithSchema, SoftDele
final public const RELATION_ANIME = 'animetheme.anime';
final public const RELATION_ANIME_SHALLOW = 'anime';
final public const RELATION_RESOURCES = 'resources';
final public const RELATION_SONG = 'animetheme.song';
final public const RELATION_SYNONYMS = 'animetheme.anime.animesynonyms';
final public const RELATION_THEME = 'animetheme';
@@ -245,6 +251,20 @@ class AnimeThemeEntry extends BaseModel implements InteractsWithSchema, SoftDele
->withTimestamps();
}
/**
* Get the resources for the entry through the resourceable morph pivot.
*
* @return MorphToMany
*/
public function resources(): MorphToMany
{
return $this->morphToMany(ExternalResource::class, Resourceable::RELATION_RESOURCEABLE, Resourceable::TABLE, Resourceable::ATTRIBUTE_RESOURCEABLE_ID, Resourceable::ATTRIBUTE_RESOURCE)
->using(Resourceable::class)
->withPivot(Resourceable::ATTRIBUTE_AS)
->as('entryresource')
->withTimestamps();
}
/**
* Get the anime that owns the entry through the theme.
*
+16
View File
@@ -13,6 +13,7 @@ use App\Events\Wiki\ExternalResource\ExternalResourceDeleted;
use App\Events\Wiki\ExternalResource\ExternalResourceRestored;
use App\Events\Wiki\ExternalResource\ExternalResourceUpdated;
use App\Models\BaseModel;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Pivots\Morph\Resourceable;
use Database\Factories\Wiki\ExternalResourceFactory;
use Illuminate\Database\Eloquent\Casts\AsUri;
@@ -49,6 +50,7 @@ class ExternalResource extends BaseModel implements SoftDeletable
final public const ATTRIBUTE_SITE = 'site';
final public const RELATION_ANIME = 'anime';
final public const RELATION_ANIMETHEMEENTRIES = 'animethemeentries';
final public const RELATION_ARTISTS = 'artists';
final public const RELATION_SONGS = 'songs';
final public const RELATION_STUDIOS = 'studios';
@@ -136,6 +138,20 @@ class ExternalResource extends BaseModel implements SoftDeletable
->withTimestamps();
}
/**
* Get the entry that reference this resource.
*
* @return MorphToMany
*/
public function animethemeentries(): MorphToMany
{
return $this->morphedByMany(AnimeThemeEntry::class, Resourceable::RELATION_RESOURCEABLE, Resourceable::TABLE, Resourceable::ATTRIBUTE_RESOURCE, Resourceable::ATTRIBUTE_RESOURCEABLE_ID)
->using(Resourceable::class)
->withPivot(Resourceable::ATTRIBUTE_AS)
->as('entryresource')
->withTimestamps();
}
/**
* Get the artists that reference this resource.
*
+2
View File
@@ -10,6 +10,7 @@ use App\Events\Pivot\Morph\Resourceable\ResourceableCreated;
use App\Events\Pivot\Morph\Resourceable\ResourceableDeleted;
use App\Events\Pivot\Morph\Resourceable\ResourceableUpdated;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Song;
@@ -51,6 +52,7 @@ class Resourceable extends BaseMorphPivot
*/
public static $resourceables = [
Anime::class,
AnimeThemeEntry::class,
Artist::class,
Song::class,
Studio::class,
+1 -1
View File
@@ -64,7 +64,7 @@ class AppServiceProvider extends ServiceProvider
DB::listen(function (QueryExecuted $query) {
if (app()->isLocal()) {
Log::debug($query->toRawSql());
Log::debug($query->sql);
}
});
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Rules\Wiki\Resource;
use App\Enums\Models\Wiki\ResourceSite;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Str;
use Illuminate\Translation\PotentiallyTranslatedString;
readonly class AnimeThemeEntryResourceLinkFormatRule implements ValidationRule
{
public function __construct(protected ResourceSite $site) {}
/**
* Run the validation rule.
*
* @param Closure(string): PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$pattern = $this->site->getPattern(AnimeThemeEntry::class);
if ($pattern !== null && Str::match($pattern, $value) !== $value) {
$fail(__('validation.regex'));
}
}
}
+3
View File
@@ -516,6 +516,9 @@ return [
'help' => 'The Version number of the Theme. Can be left blank if there is only one version. Version is only required if there exist at least 2 in the sequence.',
'name' => 'Version',
],
'youtube' => [
'help' => 'An YouTube URL to attach to the entry.',
],
],
'anime_theme' => [
'sequence' => [
@@ -5,10 +5,13 @@ declare(strict_types=1);
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Video;
use App\Pivots\Morph\Resourceable;
use App\Pivots\Wiki\AnimeThemeEntryVideo;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Foundation\Testing\WithFaker;
use Znck\Eloquent\Relations\BelongsToThrough;
@@ -55,6 +58,20 @@ test('theme', function () {
$this->assertInstanceOf(AnimeTheme::class, $entry->animetheme()->first());
});
test('external resources', function () {
$resourcesCount = fake()->randomDigitNotNull();
$entry = AnimeThemeEntry::factory()
->for(AnimeTheme::factory()->for(Anime::factory()))
->has(ExternalResource::factory()->count($resourcesCount))
->createOne();
$this->assertInstanceOf(MorphToMany::class, $entry->resources());
$this->assertEquals($resourcesCount, $entry->resources()->count());
$this->assertInstanceOf(ExternalResource::class, $entry->resources()->first());
$this->assertEquals(Resourceable::class, $entry->resources()->getPivotClass());
});
test('videos', function () {
$videoCount = fake()->randomDigitNotNull();
@@ -4,6 +4,7 @@ declare(strict_types=1);
use App\Enums\Models\Wiki\ResourceSite;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Song;
@@ -47,6 +48,19 @@ test('anime', function () {
$this->assertEquals(Resourceable::class, $resource->anime()->getPivotClass());
});
test('anime theme entry', function () {
$entryCount = fake()->randomDigitNotNull();
$resource = ExternalResource::factory()
->has(AnimeThemeEntry::factory()->forAnime()->count($entryCount))
->createOne();
$this->assertInstanceOf(MorphToMany::class, $resource->animethemeentries());
$this->assertEquals($entryCount, $resource->animethemeentries()->count());
$this->assertInstanceOf(AnimeThemeEntry::class, $resource->animethemeentries()->first());
$this->assertEquals(Resourceable::class, $resource->animethemeentries()->getPivotClass());
});
test('artists', function () {
$artistCount = fake()->randomDigitNotNull();
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
use App\Enums\Models\Wiki\ResourceSite;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Rules\Wiki\Resource\AnimeThemeEntryResourceLinkFormatRule;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
uses(Illuminate\Foundation\Testing\WithFaker::class);
test('fails for no pattern', function () {
$attribute = fake()->word();
$validator = Validator::make(
[$attribute => fake()->url()],
[$attribute => new AnimeThemeEntryResourceLinkFormatRule(ResourceSite::YOUTUBE_MUSIC)],
);
$this->assertFalse($validator->passes());
});
test('passes for pattern', function () {
/** @var ResourceSite $site */
$site = Arr::random(ResourceSite::getForModel(AnimeThemeEntry::class));
$url = $site->formatResourceLink(AnimeThemeEntry::class, fake()->randomDigitNotNull(), fake()->word(), 'null');
$attribute = fake()->word();
$validator = Validator::make(
[$attribute => $url],
[$attribute => new AnimeThemeEntryResourceLinkFormatRule($site)],
);
$this->assertTrue($validator->passes());
});
test('fails for trailing slash', function () {
/** @var ResourceSite $site */
$site = Arr::random(ResourceSite::getForModel(AnimeThemeEntry::class));
$url = $site->formatResourceLink(AnimeThemeEntry::class, fake()->randomDigitNotNull(), fake()->word());
$url = Str::of($url)
->append('/')
->__toString();
$attribute = fake()->word();
$validator = Validator::make(
[$attribute => $url],
[$attribute => new AnimeThemeEntryResourceLinkFormatRule($site)],
);
$this->assertFalse($site->getPattern(AnimeThemeEntry::class) && $validator->passes());
});
test('fails for trailing slug', function () {
/** @var ResourceSite $site */
$site = Arr::random(ResourceSite::getForModel(AnimeThemeEntry::class));
$url = $site->formatResourceLink(AnimeThemeEntry::class, fake()->randomDigitNotNull(), fake()->word());
$url = Str::of($url)
->append('/')
->append(fake()->word())
->__toString();
$attribute = fake()->word();
$validator = Validator::make(
[$attribute => $url],
[$attribute => new AnimeThemeEntryResourceLinkFormatRule($site)],
);
$this->assertFalse($site->getPattern(AnimeThemeEntry::class) && $validator->passes());
});
test('fails for other resources', function () {
/** @var ResourceSite $site */
$site = Arr::random(
array_filter(
ResourceSite::cases(),
fn ($value) => ! in_array($value, ResourceSite::getForModel(AnimeThemeEntry::class))
)
);
$url = $site->formatResourceLink(AnimeThemeEntry::class, fake()->randomDigitNotNull(), fake()->word());
$attribute = fake()->word();
$validator = Validator::make(
[$attribute => $url],
[$attribute => new AnimeThemeEntryResourceLinkFormatRule($site)],
);
$this->assertFalse($validator->passes());
});