This commit is contained in:
2024-12-09 19:12:27 +04:00
parent 78094e41c1
commit a6f655d80f
10 changed files with 574 additions and 22 deletions
+136
View File
@@ -0,0 +1,136 @@
<?php
namespace App\Command;
use App\Command\Pf2eSchemaGenerator\Schema;
use App\Command\Pf2eSchemaGenerator\SchemaItem;
use App\Service\AonprdConnectionService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'pf2e:generate',
description: 'Add a short description for your command',
)]
class Pf2eGenerateCommand extends Command
{
public function __construct(private AonprdConnectionService $connectionService)
{
parent::__construct();
}
protected function configure(): void
{
$this
->addOption("revision", null, InputOption::VALUE_REQUIRED, "Load index of specific revision. Default value - latest revision.")
->addOption('type', null, InputOption::VALUE_REQUIRED, 'Specify types to import schema')
->addOption('all', null, InputOption::VALUE_NONE, 'Generate schema for all types')
;
}
private int $revision;
/**
* @var string[]
*/
private array $types;
/**
* @var array<string, Schema>
*/
private array $schemas = [];
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$this->revision = $input->getOption('revision') ?? $this->connectionService->getRevision();
if ($input->getOption('all')) {
$this->types = [];
}
else if (is_null($input->getOption('type'))) {
// $io->error("You must specify --type=[TYPE] or --all");
// return Command::INVALID;
$this->types = ["class"];
}
else {
$this->types = explode(",", $input->getOption('type'));
}
$typePages = $this->loadTypePages();
if ($input->getOption('all')) {
$this->types = array_keys($typePages);
}
$pages = [];
foreach ($this->types as $type) {
$pages = [...$pages, ...$typePages[$type]];
$this->schemas[$type] = new Schema();
}
$pages = array_unique($pages);
foreach ($pages as $page) {
$this->loadPage($page);
}
foreach ($this->schemas as $type => $schema) {
$io->write("<options=bold,underscore>{$type}</>");
$io->writeln(str_replace(PHP_EOL, PHP_EOL.str_repeat(" ", 4), PHP_EOL.$schema));
$io->writeln("");
}
$io->success('You have a new command! Now make it your own! Pass --help to see your options.');
return Command::SUCCESS;
}
protected function loadTypePages(): array
{
$typePages = [];
$index = $this->connectionService->loadCurrentIndex($this->revision);
foreach ($index as $page => $elements) {
foreach ($elements as $element) {
preg_match("#^([a-z\\-]+?)-(\\d.*)$#", $element, $matches);
$type = $matches[1];
if (!empty($targetType) && !in_array($type, $this->types)) {
continue;
}
if (!isset($typePages[$type])) {
$typePages[$type] = [];
}
if (!in_array($page, $typePages[$type])) {
$typePages[$type][] = $page;
}
}
}
return $typePages;
}
protected function loadPage(string $page): void {
$data = $this->connectionService->load("/json-data/{$page}.json");
foreach ($data as $element) {
$type = $element["category"];
if (!in_array($type, $this->types)) {
continue;
}
$this->schemas[$type]->process($element);
}
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Command;
use App\Service\AonprdConnectionService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: "pf2e:revision",
description: "Get current revision number of Archives of Nethys",
)]
class Pf2eIndexCommand extends Command
{
public function __construct(private AonprdConnectionService $connectionService)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$revision = $this->connectionService->getRevision();
$io->success("Current Archives of Nethys' revision number is {$revision}");
return Command::SUCCESS;
}
}
@@ -0,0 +1,97 @@
<?php
namespace App\Command\Pf2eSchemaGenerator;
class Schema
{
/**
* @var SchemaItem[]
*/
private array $items = [];
public function __construct()
{
}
public function process(mixed $element): void
{
$keys = array_keys($element);
foreach ($this->items as $key => $item) {
if (!in_array($key, $keys)) {
$item->disableRequired();
}
}
foreach ($keys as $key) {
if (!array_key_exists($key, $this->items)) {
$this->items[$key] = new SchemaItem($key);
}
$this->items[$key]->adapt($element[$key]);
}
}
public static function resolveType(mixed $value): string
{
$type = gettype($value);
if ($type === "array") {
if (empty($value)) {
return "empty[]";
}
if (array_is_list($value)) {
$elementType = "";
foreach ($value as $element) {
$elementType = self::mergeTypes($elementType, self::resolveType($element));
}
if (str_contains("|", $elementType)) {
return "mixed[]";
}
return $elementType . "[]";
}
return "object";
}
return $type;
}
public static function mergeTypes(string $typeList, string $newType): string {
if (empty($typeList)) {
return $newType;
}
else if (str_contains("|", $typeList)) {
$types = explode("|", $typeList);
if (!in_array($newType, $types, true)) {
$types[] = $newType;
}
return implode("|", $types);
}
else if ($newType !== $typeList) {
return $typeList . "|" . $newType;
}
return $typeList;
}
public function __toString(): string
{
$rows = [];
foreach ($this->items as $item) {
$rows = [...$rows, ...$item->strings()];
}
return implode(PHP_EOL, $rows);
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Command\Pf2eSchemaGenerator;
class SchemaItem
{
public string $name;
public string $type = "";
public bool $required = true;
public ?array $elements = [];
public function __construct($name)
{
$this->name = $name;
}
public function disableRequired(): void
{
$this->required = false;
}
public function adapt(mixed $value): void
{
$type = Schema::resolveType($value);
$this->type = Schema::mergeTypes($this->type, $type);
if (str_contains($this->type, "[]") && !is_null($this->elements)) {
foreach ($value as $element) {
if (!in_array($element, $this->elements)) {
$this->elements[] = $element;
}
}
if (count($this->elements) > 20) {
$this->elements = null;
}
}
}
public function strings(): array {
$name = $this->name;
if (!$this->required) {
$name = "[{$name}]";
}
$rows = [
sprintf("%s: %s", $name, $this->type),
];
if (!empty($this->elements)) {
sort($this->elements);
$elements = [];
foreach ($this->elements as $element) {
$elements[] = is_string($element) ? "\"{$element}\"" : strval($element);
}
$rows[] = " Elements: [ " . implode(", ", $elements) . " ]";
}
return $rows;
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace App\Command;
use App\Service\AonprdConnectionService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'pf2e:types',
description: 'Load types from Archives of Nethys',
)]
class Pf2eTypesCommand extends Command
{
public function __construct(private AonprdConnectionService $connectionService)
{
parent::__construct();
}
protected function configure(): void
{
$this
->addOption("revision", null, InputOption::VALUE_REQUIRED, "Load index of specific revision. Default value - latest revision.")
->addOption("full", null, InputOption::VALUE_NONE, "Display all columns")
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$revision = $input->getOption("revision");
$full = $input->getOption("full") ?? false;
$index = $this->connectionService->loadCurrentIndex($revision);
$types = [];
foreach ($index as $page) {
foreach ($page as $element) {
preg_match("#^([a-z\\-]+?)-(\\d.*)$#", $element, $matches);
$type = $matches[1];
$id = $matches[2];
if (!isset($types[$type])) {
$types[$type] = [
"name" => $type,
"first_id" => $element,
"count" => 0,
"templates" => []
];
}
$template = preg_replace("#\\d+#", "%d", $id);
if (!in_array($template, $types[$type]['templates'])) {
$types[$type]['templates'][] = $template;
}
$types[$type]['count']++;
}
}
if ($full) {
$table = [];
foreach ($types as $key => $type) {
$table[] = [
$key,
sprintf("<href=https://elasticsearch.aonprd.com/aon/_doc/%s>%s</>",
$type["first_id"],
$type["first_id"]
),
$type['count'],
implode(", ", $type['templates']),
];
}
$io->table(["Type name", "First Id", "Amount", "ID templates"], $table);
}
else {
foreach ($types as $type) {
$io->writeln($type['name']);
}
}
return Command::SUCCESS;
}
}
@@ -1,8 +1,8 @@
<?php
namespace App\Entity\Pf2e;
namespace App\Entity;
use App\Repository\Pf2e\ItemRepository;
use App\Repository\ItemRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace App\Entity;
enum ItemTypeEnum: string {
case Action = 'action';
case Ancestry = 'ancestry';
case AnimalCompanion = 'animal-companion';
case AnimalCompanionAdvanced = 'animal-companion-advanced';
case AnimalCompanionSpecialization = 'animal-companion-specialization';
case AnimalCompanionUnique = 'animal-companion-unique';
case ArcaneSchool = 'arcane-school';
case ArcaneThesis = 'arcane-thesis';
case Archetype = 'archetype';
case Armor = 'armor';
case ArmorGroup = 'armor-group';
case Article = 'article';
case Background = 'background';
case Bloodline = 'bloodline';
case CampsiteMeal = 'campsite-meal';
case CategoryPage = 'category-page';
case Cause = 'cause';
case Klass = 'class';
case KKlassFeature = 'class-feature';
case KlassKit = 'class-kit';
case KlassSample = 'class-sample';
case Condition = 'condition';
case ConsciousMind = 'conscious-mind';
case Creature = 'creature';
case CreatureAbility = 'creature-ability';
case CreatureAdjustment = 'creature-adjustment';
case CreatureFamily = 'creature-family';
case CreatureThemeTemplate = 'creature-theme-template';
case Curse = 'curse';
case Deity = 'deity';
case DeityCategory = 'deity-category';
case DeviantAbilityClassification = 'deviant-ability-classification';
case Disease = 'disease';
case Doctrine = 'doctrine';
case Domain = 'domain';
case DruidicOrder = 'druidic-order';
case Eidolon = 'eidolon';
case Element = 'element';
case Equipment = 'equipment';
case EquipmentCategory = 'equipment-category';
case FamiliarAbility = 'familiar-ability';
case FamiliarSpecific = 'familiar-specific';
case Feat = 'feat';
case Hazard = 'hazard';
case HellknightOrder = 'hellknight-order';
case Heritage = 'heritage';
case HuntersEdge = 'hunters-edge';
case HybridStudy = 'hybrid-study';
case Implement = 'implement';
case Innovation = 'innovation';
case Instinct = 'instinct';
case KingdomEvent = 'kingdom-event';
case KingdomStructure = 'kingdom-structure';
case Language = 'language';
case Lesson = 'lesson';
case Methodology = 'methodology';
case Muse = 'muse';
case Mystery = 'mystery';
case Patron = 'patron';
case Plane = 'plane';
case Racket = 'racket';
case Relic = 'relic';
case ResearchField = 'research-field';
case Ritual = 'ritual';
case Rules = 'rules';
case SetRelic = 'set-relic';
case Shield = 'shield';
case Sidebar = 'sidebar';
case SiegeWeapon = 'siege-weapon';
case Skill = 'skill';
case SkillGeneralAction = 'skill-general-action';
case Source = 'source';
case Spell = 'spell';
case Style = 'style';
case SubconsciousMind = 'subconscious-mind';
case Tenet = 'tenet';
case Tradition = 'tradition';
case Trait = 'trait';
case Vehicle = 'vehicle';
case WarfareArmy = 'warfare-army';
case WarfareTactic = 'warfare-tactic';
case Way = 'way';
case Weapon = 'weapon';
case WeaponGroup = 'weapon-group';
case WeatherHazard = 'weather-hazard';
}
-18
View File
@@ -1,18 +0,0 @@
<?php
namespace App\Entity\Pf2e;
enum ItemTypeEnum: string {
case Feat = 'feat';
case Ancestry = 'ancestry';
case Heritage = 'heritage';
case Archetype = 'archetype';
case Background = 'background';
case Skill = 'skill';
case Klass = 'klass';
case SubKlass = 'sub-klass';
case Equipment = 'equipment';
case Trait = 'trait';
case Rule = 'rule';
}
@@ -1,8 +1,8 @@
<?php
namespace App\Repository\Pf2e;
namespace App\Repository;
use App\Entity\Pf2e\Item;
use App\Entity\Item;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace App\Service;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class AonprdConnectionService
{
public const DATA_LINK = "https://elasticsearch.aonprd.com";
private ?int $revision = null;
public function __construct(private HttpClientInterface $client)
{
}
public function load($url): array
{
if (!str_starts_with($url, 'https://') && !str_starts_with($url, 'http://')) {
$url = self::DATA_LINK . (str_starts_with($url, "/") ? "" : "/") . $url;
}
$response = $this->client->request('GET', $url);
return json_decode($response->getContent(false), true);
}
public function getRevision(): int
{
if (null === $this->revision) {
$data = $this->load("/aon/_doc/feat-1");
$this->revision = intval(str_replace("aon", "", $data["_index"]));
}
return $this->revision;
}
public function loadCurrentIndex(int $revision = null): array
{
if ($revision === null) {
$revision = $this->getRevision();
}
$data = $this->load("/json-data/aon{$revision}-index.json");
return $data;
}
}